Интеграция с картой

Интеграция Mapbox GL JS с веб-приложением начинается с создания DOM-элемента, который будет служить контейнером для WebGL-карты. Этот контейнер должен иметь явно заданные размеры, иначе рендеринг карты не произойдёт корректно.

<div id="map"></div>
#map {
  width: 100%;
  height: 100vh;
}

Ключевым условием работы является подключение библиотеки и указание access token, выдаваемого платформой Mapbox.

import mapboxgl from 'mapbox-gl';

mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';

Инициализация карты выполняется через конструктор Map, которому передаётся конфигурационный объект:

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

Контейнер и связь с DOM

Параметр container принимает либо строковый идентификатор элемента, либо сам DOM-узел. При интеграции в SPA-фреймворки предпочтительнее использовать прямую ссылку на элемент, чтобы исключить проблемы повторного рендера.

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

const map = new mapboxgl.Map({
  container,
  style: 'mapbox://styles/mapbox/light-v11'
});

Особенность WebGL-рендеринга заключается в том, что карта создаёт собственный canvas-слой внутри контейнера, полностью контролируя отрисовку.


Жизненный цикл карты и событие загрузки

Инициализация карты не означает её готовность к работе. Основная конфигурация и тайлы загружаются асинхронно. Для корректного добавления слоёв и источников используется событие load.

map.on('load', () => {
  map.addSource('points', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: []
    }
  });
});

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

События жизненного цикла

Карта предоставляет набор событий, отражающих состояние рендеринга:

  • load — завершена загрузка стиля
  • render — происходит каждый кадр отрисовки
  • idle — нет активных обновлений
  • remove — карта уничтожена

Использование idle полезно для выполнения операций после полной стабилизации карты:

map.on('idle', () => {
  console.log('Карта полностью загружена и стабилизирована');
});

Связь карты с внешним состоянием приложения

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

Пример синхронизации фильтра данных

function updateFilter(category) {
  map.setFilter('points-layer', [
    '==',
    ['get', 'category'],
    category
  ]);
}

В этом подходе карта получает только декларативное описание фильтрации, без необходимости перерисовки всего источника данных.


Управление камерой и интеграция с интерфейсом

Камера карты представляет собой абстракцию, включающую центр, масштаб, наклон и поворот. Управление осуществляется через методы flyTo, easeTo, jumpTo.

Плавная навигация

map.flyTo({
  center: [30.3158, 59.9391],
  zoom: 12,
  speed: 1.2,
  curve: 1.4
});

Привязка к UI элементам

document.getElementById('btn-spb').addEventListener('click', () => {
  map.easeTo({
    center: [30.3158, 59.9391],
    zoom: 11
  });
});

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


Динамическое добавление слоёв

Слои являются основным механизмом визуализации данных. Каждый слой привязан к источнику данных (source) и описывает способ отображения.

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

map.on('load', () => {
  map.addSource('cities', {
    type: 'geojson',
    data: '/data/cities.geojson'
  });

  map.addLayer({
    id: 'cities-layer',
    type: 'circle',
    source: 'cities',
    paint: {
      'circle-radius': 6,
      'circle-color': '#ff5500'
    }
  });
});

Обновление данных без перезагрузки карты

map.getSource('cities').setData(newGeoJsonData);

Этот механизм позволяет интегрировать карту с потоковыми данными, REST API и WebSocket-каналами.


Интеграция с внешними API и потоковыми данными

Карты часто используются как визуализация динамических данных: транспорт, IoT, геотрекинг. В таких сценариях важно минимизировать перерисовки.

Пример WebSocket обновления

const socket = new WebSocket('wss://example.com/geo');

socket.onmess age = (event) => {
  const data = JSON.parse(event.data);
  map.getSource('vehicles').setData(data);
};

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


Встраивание карты в компонентную архитектуру

При использовании React, Vue или аналогичных фреймворков карта должна инициализироваться строго после монтирования DOM-узла.

Пример жизненного цикла (абстрактно)

let mapInstance;

function initMap(container) {
  mapInstance = new mapboxgl.Map({
    container,
    style: 'mapbox://styles/mapbox/dark-v11'
  });
}

Важно предотвращать повторную инициализацию при повторных рендерах компонента.

Очистка ресурсов

function destroyMap() {
  if (mapInstance) {
    mapInstance.remove();
    mapInstance = null;
  }
}

Метод remove() освобождает WebGL-контекст и уничтожает внутренние обработчики событий.


Обработка размеров и адаптивность

Карта не отслеживает изменения контейнера автоматически. При изменении размеров окна необходимо явно вызывать resize.

window.addEventListener('resize', () => {
  map.resize();
});

При использовании flex- или grid-лейаутов это особенно важно, поскольку контейнер может менять размеры без перезагрузки страницы.


Перекрытие карты интерфейсными слоями

Интеграция часто включает наложение UI поверх карты: панели, тултипы, списки объектов.

Абсолютное позиционирование интерфейса

.map-ui {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
}

Карта всегда остаётся в нижнем слое, так как canvas имеет базовый контекст рендеринга.


Синхронизация нескольких карт

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

const mapA = new mapboxgl.Map({
  container: 'mapA',
  style: 'mapbox://styles/mapbox/streets-v12'
});

const mapB = new mapboxgl.Map({
  container: 'mapB',
  style: 'mapbox://styles/mapbox/satellite-v9'
});

Синхронизация камеры осуществляется вручную через обработчики событий:

mapA.on('move', () => {
  const center = mapA.getCenter();
  mapB.setCenter(center);
});

Работа с несколькими стилями в одном приложении

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

map.setStyle('mapbox://styles/mapbox/outdoors-v12');

map.once('style.load', () => {
  map.addSource(...);
  map.addLayer(...);
});

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


Привязка интерактивных элементов к геоданным

Интерактивность реализуется через события карты и слоёв.

map.on('click', 'cities-layer', (e) => {
  const feature = e.features[0];

  new mapboxgl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML(`<strong>${feature.properties.name}</strong>`)
    .addTo(map);
});

Слои становятся интерактивными только при явном указании параметра interactive через обработчики событий.


Управление производительностью при интеграции

Карта использует WebGL, поэтому основная нагрузка ложится на GPU. Однако интеграция с приложением может создавать узкие места при частых обновлениях данных.

Рекомендованные подходы:

  • обновление source вместо пересоздания слоёв
  • батчинг входящих данных
  • ограничение частоты вызова setData
  • использование requestAnimationFrame для синхронизации обновлений
let pendingData = null;

function scheduleUpdate(data) {
  pendingData = data;

  requestAnimationFrame(() => {
    if (pendingData) {
      map.getSource('points').setData(pendingData);
      pendingData = null;
    }
  });
}

Интеграция с пользовательскими контролами

Карта поддерживает кастомные контролы, которые интегрируются как DOM-элементы.

class CustomControl {
  onAdd(map) {
    this.container = document.createElement('div');
    this.container.textContent = 'Control';
    return this.container;
  }

  onRemove() {
    this.container.remove();
  }
}

map.addControl(new CustomControl(), 'top-right');

Контролы позволяют связывать карту с бизнес-логикой приложения без нарушения архитектуры рендеринга.