Пошаговая навигация

Пошаговая навигация (Turn-by-Turn Navigation) представляет собой процесс построения маршрута между точками и последовательного отображения инструкций движения: поворотов, съездов, разворотов, изменения полос движения и других манёвров. В веб-приложениях на базе Mapbox GL JS такая функциональность строится за счёт совместного использования картографического движка и сервисов маршрутизации.

Mapbox GL JS отвечает за:

  • отображение карты;
  • визуализацию маршрута;
  • отображение маркеров;
  • анимацию перемещения;
  • работу со слоями и источниками данных.

Сервис маршрутизации предоставляет:

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

Схема работы выглядит следующим образом:

  1. Пользователь задаёт начальную и конечную точки.
  2. Приложение отправляет запрос к сервису маршрутизации.
  3. Получается маршрут в формате GeoJSON.
  4. Маршрут отображается на карте.
  5. Инструкции навигации выводятся в интерфейс.
  6. При изменении положения пользователя вычисляется текущий шаг маршрута.

Архитектура навигационного приложения

Типичная структура состоит из нескольких компонентов.

Карта

Создаётся экземпляр Mapbox GL JS:

mapboxgl.accessToken = 'YOUR_TOKEN';

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [37.6176, 55.7558],
    zoom: 12
});

Точки маршрута

Необходимо хранить:

const start = [37.6176, 55.7558];
const destination = [37.6810, 55.7600];

Навигационный сервис

Отвечает за получение маршрута:

async function getRoute(start, destination) {
    const url =
        `https://api.mapbox.com/directions/v5/mapbox/driving/` +
        `${start[0]},${start[1]};${destination[0]},${destination[1]}` +
        `?steps=true&geometries=geojson&access_token=${mapboxgl.accessToken}`;

    const response = await fetch(url);

    return response.json();
}

Навигационный интерфейс

Может содержать:

  • список манёвров;
  • текущее расстояние до поворота;
  • предполагаемое время прибытия;
  • индикатор следующего действия.

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

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

Запрос должен содержать параметр:

steps=true

Полный пример:

const response = await fetch(
    `https://api.mapbox.com/directions/v5/mapbox/driving/
    37.6176,55.7558;
    37.6810,55.7600
    ?steps=true
    &geometries=geojson
    &overview=full
    &access_token=${mapboxgl.accessToken}`
);

const data = await response.json();

Из ответа извлекается маршрут:

const route = data.routes[0];

Информация о маршруте:

console.log(route.distance);
console.log(route.duration);

Структура данных маршрута

Маршрут содержит массив участков:

route.legs

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

route.legs[0].steps

Пример шага:

{
    distance: 120,
    duration: 15,
    geometry: {...},
    maneuver: {
        instruction: "Поверните направо",
        location: [37.620, 55.756]
    }
}

Наиболее важные поля:

Поле Описание
distance расстояние шага
duration время выполнения
geometry геометрия участка
maneuver информация о манёвре
instruction текст инструкции
location координаты манёвра

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

После получения геометрии её необходимо вывести как слой.

Создание источника:

map.addSource('route', {
    type: 'geojson',
    data: {
        type: 'Feature',
        geometry: route.geometry
    }
});

Создание линии маршрута:

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

В результате появляется визуальное представление пути.


Отображение начальной и конечной точек

Для улучшения восприятия маршрута обычно добавляются маркеры.

Начальная точка:

new mapboxgl.Marker({
    color: 'green'
})
.setLngLat(start)
.addTo(map);

Конечная точка:

new mapboxgl.Marker({
    color: 'red'
})
.setLngLat(destination)
.addTo(map);

Формирование списка инструкций

Навигационные шаги можно преобразовать в удобный массив.

const steps = route.legs[0].steps;

Получение инструкций:

const instructions = steps.map(step => ({
    text: step.maneuver.instruction,
    distance: step.distance
}));

Результат:

[
    {
        text: "Двигайтесь прямо",
        distance: 200
    },
    {
        text: "Поверните направо",
        distance: 150
    }
]

Вывод инструкций в HTML

Создание списка:

const container =
    document.getElementById('instructions');

Заполнение:

steps.forEach(step => {
    const item = document.createElement('div');

    item.textContent =
        `${step.maneuver.instruction}
        (${Math.round(step.distance)} м)`;

    container.appendChild(item);
});

HTML:

<div id="instructions"></div>

Определение текущего шага маршрута

Для пошаговой навигации необходимо понимать, возле какого манёвра находится пользователь.

Положение пользователя:

const userPosition =
    [37.6200, 55.7560];

Координаты следующего манёвра:

const maneuverLocation =
    currentStep.maneuver.location;

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

Для этого часто используется библиотека Turf.js.


Вычисление расстояния до следующего манёвра

Подключение Turf:

<script src="https://unpkg.com/@turf/turf"></script>

Расстояние:

const from =
    turf.point(userPosition);

const to =
    turf.point(maneuverLocation);

const distance =
    turf.distance(from, to, {
        units: 'meters'
    });

Проверка:

if (distance < 20) {
    moveToNextStep();
}

Когда пользователь оказывается рядом с манёвром, система переключается на следующий шаг.


Автоматическое переключение инструкций

Текущий индекс шага:

let currentStepIndex = 0;

Получение инструкции:

function getCurrentStep() {
    return steps[currentStepIndex];
}

Переход:

function moveToNextStep() {
    currentStepIndex++;

    updateInstruction();
}

Обновление интерфейса:

function updateInstruction() {
    const step = getCurrentStep();

    instructionElement.textContent =
        step.maneuver.instruction;
}

Отображение оставшегося расстояния

Расстояние до манёвра может обновляться в режиме реального времени.

distanceElement.textContent =
    `${Math.round(distance)} м`;

Типичный интерфейс:

Через 150 м поверните налево

или

Через 40 м поверните направо

Работа с Geolocation API

Для реальной навигации необходимо отслеживать местоположение устройства.

Запуск отслеживания:

navigator.geolocation.watchPosition(
    position => {

        const lng =
            position.coords.longitude;

        const lat =
            position.coords.latitude;

        updateNavigation([lng, lat]);
    },
    error => {
        console.error(error);
    },
    {
        enableHighAccuracy: true
    }
);

Каждое обновление координат запускает пересчёт навигации.


Перемещение маркера пользователя

Создание маркера:

const userMarker =
    new mapboxgl.Marker({
        color: 'blue'
    })
    .setLngLat(start)
    .addTo(map);

Обновление положения:

userMarker.setLngLat([
    lng,
    lat
]);

Пользователь визуально перемещается по маршруту.


Центрирование карты на пользователе

При движении карта может следовать за объектом.

map.easeTo({
    center: [lng, lat],
    zoom: 17,
    duration: 500
});

Такой подход создаёт эффект автомобильного навигатора.


Поворот карты по направлению движения

Для повышения удобства карта может автоматически вращаться.

Пример:

map.easeTo({
    bearing: heading,
    duration: 300
});

Где:

heading =
position.coords.heading;

Если устройство поддерживает определение направления движения, карта всегда будет ориентирована вперёд по маршруту.


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

Пользователь может отклониться от рассчитанного пути.

Для обнаружения используется расстояние до линии маршрута.

Создание точки:

const point =
    turf.point(userPosition);

Создание линии:

const line =
    turf.lineString(
        route.geometry.coordinates
    );

Расстояние до маршрута:

const distance =
    turf.pointToLineDistance(
        point,
        line,
        {
            units: 'meters'
        }
    );

Проверка:

if (distance > 50) {
    recalculateRoute();
}

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

Функция может использовать текущее положение как новую стартовую точку.

async function recalculateRoute() {

    const routeData =
        await getRoute(
            currentPosition,
            destination
        );

    drawRoute(routeData);
}

Подобное поведение характерно для автомобильных навигаторов.


Анимация движения по маршруту

Для демонстрационных проектов полезно анимировать объект вдоль линии маршрута.

Получение координат:

const coordinates =
    route.geometry.coordinates;

Перемещение:

let index = 0;

function animate() {

    if (index >= coordinates.length) {
        return;
    }

    userMarker.setLngLat(
        coordinates[index]
    );

    index++;

    requestAnimationFrame(
        animate
    );
}

Запуск:

animate();

Голосовые инструкции

Пошаговая навигация часто дополняется озвучиванием манёвров.

Использование Web Speech API:

function speak(text) {

    const utterance =
        new SpeechSynthesisUtterance(
            text
        );

    speechSynthesis.speak(
        utterance
    );
}

Пример:

speak(
    "Через сто метров поверните направо"
);

Озвучивание может запускаться при достижении определённого расстояния до манёвра.


Отображение информации о поездке

Дополнительная статистика обычно выводится в отдельном блоке.

Расстояние:

const km =
    (route.distance / 1000)
    .toFixed(1);

Время:

const minutes =
    Math.round(
        route.duration / 60
    );

Отображение:

tripInfo.innerHTML = `
    <strong>${km} км</strong>
    <br>
    ${minutes} мин
`;

Навигация с несколькими промежуточными точками

Маршрут может содержать несколько остановок.

Пример координат:

const points = [
    [37.6176, 55.7558],
    [37.6400, 55.7600],
    [37.6600, 55.7700],
    [37.6810, 55.7600]
];

Формирование строки координат:

const coordinates =
    points
    .map(p => p.join(','))
    .join(';');

Запрос:

const url =
`https://api.mapbox.com/directions/v5/
mapbox/driving/${coordinates}
?steps=true
&geometries=geojson
&access_token=${mapboxgl.accessToken}`;

В результате формируется сложный маршрут с несколькими участками и собственными наборами инструкций для каждого сегмента.


Оптимизация производительности

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

  • хранить маршрут в памяти и не выполнять повторные запросы без необходимости;
  • обновлять интерфейс только при смене шага;
  • ограничивать частоту обновления геолокации;
  • использовать один источник данных для маршрута вместо создания новых;
  • применять setData() для обновления GeoJSON;
  • отключать лишние анимации на мобильных устройствах;
  • выполнять перестроение маршрута только после значительного отклонения пользователя.

Грамотно реализованная пошаговая навигация в Mapbox GL JS позволяет создавать полноценные навигационные веб-приложения с отображением маршрутов, автоматическим сопровождением движения, голосовыми подсказками, отслеживанием положения пользователя и динамическим перестроением пути в режиме реального времени.