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

Отображение маршрута на карте является одной из наиболее востребованных задач при разработке геоинформационных веб-приложений. Маршрут может представлять собой автомобильный путь, велосипедную трассу, пешеходное направление, логистическую цепочку доставки или произвольную линию движения объекта.

В Mapbox GL JS маршрут обычно отображается как геометрия типа LineString, которая визуализируется через отдельный слой карты. Источником данных может выступать:

  • статический набор координат;
  • результат вычисления маршрута через API маршрутизации;
  • данные GPS-трека;
  • потоковые координаты в режиме реального времени.

Представление маршрута в формате GeoJSON

Mapbox GL JS использует формат GeoJSON в качестве основного механизма передачи пространственных данных.

Простейший маршрут состоит из массива координат, объединённых в объект типа LineString.

const route = {
    type: "Feature",
    geometry: {
        type: "LineString",
        coordinates: [
            [37.6176, 55.7558],
            [37.6200, 55.7575],
            [37.6250, 55.7600],
            [37.6300, 55.7625]
        ]
    }
};

Каждая координата записывается в формате:

[долгота, широта]

Важно помнить, что порядок координат в GeoJSON отличается от привычного представления географических координат.

Правильно:

[37.6176, 55.7558]

Неправильно:

[55.7558, 37.6176]

Создание карты

Перед отображением маршрута необходимо создать экземпляр карты.

mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';

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

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

map.on('load', () => {

});

Событие load гарантирует, что стиль карты полностью загружен и готов к работе.


Добавление маршрута как источника данных

Сначала создаётся источник GeoJSON.

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

Источник представляет собой контейнер данных, который затем используется одним или несколькими слоями.

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

GeoJSON
    ↓
Source
    ↓
Layer
    ↓
Отображение на карте

Создание линейного слоя

После регистрации источника необходимо создать слой визуализации.

map.addLayer({
    id: 'route-line',
    type: 'line',
    source: 'route',
    paint: {
        'line-color': '#0078ff',
        'line-width': 5
    }
});

Результатом станет синяя линия, соединяющая все точки маршрута.


Настройка цвета маршрута

Цвет задаётся через свойство:

'line-color'

Примеры:

'line-color': '#ff0000'
'line-color': '#00aa55'
'line-color': 'orange'

Также допускается использование выражений Mapbox Expression.

Например, выбор цвета по свойствам объекта:

'line-color': [
    'match',
    ['get', 'status'],
    'active',
    '#00cc66',
    '#ff4444'
]

Настройка толщины линии

Толщина маршрута определяется свойством:

'line-width'

Пример:

'line-width': 8

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

'line-width': [
    'interpolate',
    ['linear'],
    ['zoom'],
    5, 2,
    10, 5,
    15, 10
]

При увеличении масштаба линия будет становиться толще.


Скругление углов маршрута

По умолчанию соединения сегментов могут выглядеть достаточно жёстко.

Для более плавного отображения используются настройки:

layout: {
    'line-join': 'round',
    'line-cap': 'round'
}

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

map.addLayer({
    id: 'route-line',
    type: 'line',
    source: 'route',
    layout: {
        'line-join': 'round',
        'line-cap': 'round'
    },
    paint: {
        'line-color': '#0078ff',
        'line-width': 6
    }
});

Параметры:

Свойство Назначение
line-join Соединение сегментов
line-cap Окончания линии

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

Для обозначения альтернативного пути часто используются пунктирные линии.

paint: {
    'line-color': '#ff5500',
    'line-width': 4,
    'line-dasharray': [2, 2]
}

Возможные варианты:

[1, 1]
[2, 4]
[5, 2]

Полупрозрачный маршрут

Прозрачность задаётся через:

'line-opacity'

Пример:

paint: {
    'line-color': '#0066ff',
    'line-width': 6,
    'line-opacity': 0.5
}

Значение находится в диапазоне:

0.0 — полностью прозрачный
1.0 — полностью видимый

Добавление стартовой и конечной точки

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

Источник данных:

const points = {
    type: "FeatureCollection",
    features: [
        {
            type: "Feature",
            properties: {
                type: "start"
            },
            geometry: {
                type: "Point",
                coordinates: [37.6176, 55.7558]
            }
        },
        {
            type: "Feature",
            properties: {
                type: "finish"
            },
            geometry: {
                type: "Point",
                coordinates: [37.6300, 55.7625]
            }
        }
    ]
};

Добавление источника:

map.addSource('points', {
    type: 'geojson',
    data: points
});

Слой отображения:

map.addLayer({
    id: 'points-layer',
    type: 'circle',
    source: 'points',
    paint: {
        'circle-radius': 8,
        'circle-color': [
            'match',
            ['get', 'type'],
            'start',
            '#00cc66',
            '#ff3333'
        ]
    }
});

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

После загрузки маршрута желательно показать его целиком.

Для этого используется объект LngLatBounds.

const bounds = new mapboxgl.LngLatBounds();

route.geometry.coordinates.forEach(coord => {
    bounds.extend(coord);
});

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

Параметр:

padding: 50

создаёт внутренний отступ от границ окна карты.


Вычисление маршрута через Mapbox Directions API

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

Маршрут строится через сервис маршрутизации Mapbox.

Пример URL-запроса:

const url =
`https://api.mapbox.com/directions/v5/mapbox/driving/
37.6176,55.7558;
37.6300,55.7625
?geometries=geojson
&access_token=${mapboxgl.accessToken}`;

Получение данных:

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

Извлечение маршрута:

const route = data.routes[0].geometry;

Полученная геометрия уже имеет формат GeoJSON и может быть сразу отображена на карте.


Добавление маршрута из API

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

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

Далее создаётся стандартный линейный слой.

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

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

Часто требуется перестраивать маршрут после изменения точек.

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

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

Преимущества подхода:

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

Анимация построения маршрута

Иногда требуется эффект постепенного прорисовывания пути.

Создаётся массив координат.

const fullRoute = route.geometry.coordinates;

Начальное состояние:

const animatedRoute = {
    type: 'Feature',
    geometry: {
        type: 'LineString',
        coordinates: []
    }
};

Постепенное добавление точек:

let index = 0;

function animateRoute() {

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

    animatedRoute.geometry.coordinates.push(
        fullRoute[index]
    );

    map.getSource('route').setData(animatedRoute);

    index++;

    requestAnimationFrame(animateRoute);
}

animateRoute();

Линия будет постепенно удлиняться вдоль маршрута.


Отображение нескольких маршрутов

Иногда необходимо показывать несколько вариантов пути.

Источник:

const routes = {
    type: "FeatureCollection",
    features: [
        route1,
        route2,
        route3
    ]
};

Добавление:

map.addSource('routes', {
    type: 'geojson',
    data: routes
});

Слой:

map.addLayer({
    id: 'routes',
    type: 'line',
    source: 'routes',
    paint: {
        'line-width': 5,
        'line-color': [
            'match',
            ['get', 'selected'],
            true,
            '#0078ff',
            '#bbbbbb'
        ]
    }
});

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


Выделение активного маршрута

Маршрут может содержать дополнительные свойства.

{
    type: "Feature",
    properties: {
        active: true
    },
    geometry: {
        ...
    }
}

Использование свойства в стиле:

'line-width': [
    'case',
    ['get', 'active'],
    8,
    4
]

или

'line-opacity': [
    'case',
    ['get', 'active'],
    1,
    0.4
]

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


Добавление подписи с длиной маршрута

После получения ответа от Directions API доступна информация о расстоянии.

const distance =
    data.routes[0].distance;

Значение возвращается в метрах.

Перевод в километры:

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

Создание всплывающей подсказки:

new mapboxgl.Popup()
    .setLngLat(
        route.coordinates[
            Math.floor(
                route.coordinates.length / 2
            )
        ]
    )
    .setHTML(`
        <b>Маршрут</b><br>
        ${km} км
    `)
    .addTo(map);

Отображение движения объекта по маршруту

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

Создаётся маркер.

const marker = new mapboxgl.Marker()
    .setLngLat(
        route.geometry.coordinates[0]
    )
    .addTo(map);

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

let step = 0;

function move() {

    if (step >= route.geometry.coordinates.length) {
        return;
    }

    marker.setLngLat(
        route.geometry.coordinates[step]
    );

    step++;

    requestAnimationFrame(move);
}

move();

Подобный механизм применяется для:

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

Оптимизация отображения длинных маршрутов

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

Основные способы оптимизации:

Упрощение геометрии

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

  • Douglas-Peucker;
  • Visvalingam-Whyatt;
  • Simplify.js.

Ограничение количества обновлений

Вместо непрерывного вызова:

setData(...)

целесообразно обновлять данные через интервалы времени.

setInterval(updateRoute, 500);

Использование одного источника

Предпочтительно хранить все маршруты в одном GeoJSON Source.

FeatureCollection

работает эффективнее, чем большое количество отдельных источников.

Минимизация числа слоёв

Лучше использовать выражения Mapbox Style Expressions для изменения внешнего вида объектов внутри одного слоя, чем создавать множество однотипных слоёв.


Типовая архитектура отображения маршрутов

В большинстве приложений процесс построения маршрута выглядит следующим образом:

Пользователь выбирает точки
            ↓
Запрос к Directions API
            ↓
Получение GeoJSON маршрута
            ↓
Создание или обновление Source
            ↓
Отрисовка Line Layer
            ↓
Масштабирование fitBounds()
            ↓
Отображение маркеров начала и конца
            ↓
Дополнительная анимация и интерактивность

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