Миграция с Leaflet

Leaflet построен вокруг императивного управления картой с использованием тайлов и DOM-слоёв. Основная модель — набор слоёв, каждый из которых рендерится как HTML или Canvas поверх тайловой подложки. Вся логика взаимодействия с картой сводится к добавлению и удалению объектов через API слоёв и маркеров.

Mapbox GL JS использует WebGL-рендеринг и декларативную модель стилей. Карта описывается как набор источников данных (sources) и слоёв (layers), а визуализация управляется через стиль JSON-формата. Это принципиально меняет подход: вместо управления объектами на карте напрямую, происходит управление описанием того, как данные должны быть отрисованы.

Ключевое различие:

  • Leaflet: объектно-ориентированная модель (маркер, полилиния, слой)
  • Mapbox GL JS: стиль-ориентированная модель (source + layer + paint/layout properties)

Модель данных: переход от объектов к источникам

В Leaflet геометрия добавляется напрямую:

L.marker([51.5, -0.09]).addTo(map);
L.polyline([[51.5, -0.09], [51.51, -0.1]]).addTo(map);

Каждый объект существует независимо и управляется через API экземпляра.

В Mapbox GL JS данные разделяются на источники:

map.addSource('points', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [-0.09, 51.5]
        }
      }
    ]
  }
});

И затем отображаются через слой:

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

Следствие миграции

При переходе необходимо:

  • заменить маркеры и линии на GeoJSON-источники
  • перевести UI-объекты в слои
  • отказаться от прямого DOM-манипулирования поверх карты

Маркеры: переход от DOM к слоям или HTML-объектам

В Leaflet маркеры являются DOM-элементами:

L.marker([51.5, -0.09])
  .bindPopup('Точка')
  .addTo(map);

Они легко кастомизируются через HTML и CSS.

В Mapbox GL JS есть два подхода:

1. HTML-маркеры (аналог Leaflet)

const el = document.createElement('div');
el.className = 'marker';

new mapboxgl.Marker(el)
  .setLngLat([-0.09, 51.5])
  .addTo(map);

2. Векторные маркеры через слои

map.addLayer({
  id: 'points',
  type: 'circle',
  source: 'points',
  paint: {
    'circle-radius': 8,
    'circle-color': '#3b82f6'
  }
});

Различие подходов

  • HTML-маркеры: удобны для UI, но хуже масштабируются
  • слои WebGL: производительнее при большом количестве объектов

Работа с событиями

Leaflet использует событийную модель:

map.on('click', function(e) {
  console.log(e.latlng);
});

Mapbox GL JS разделяет события карты и события слоёв:

map.on('click', 'points-layer', (e) => {
  console.log(e.features[0]);
});

Особенность:

  • события могут быть привязаны к слоям
  • доступ к данным осуществляется через features

Работа с GeoJSON

В Leaflet GeoJSON используется как слой:

L.geoJSON(data).addTo(map);

В Mapbox GL JS GeoJSON становится источником:

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

и визуализируется отдельно:

map.addLayer({
  id: 'data-fill',
  type: 'fill',
  source: 'data',
  paint: {
    'fill-color': '#088',
    'fill-opacity': 0.5
  }
});

Ключевая трансформация

  • Leaflet: GeoJSON = слой
  • Mapbox GL JS: GeoJSON = источник + набор слоёв

Работа с тайлами и базовой картой

Leaflet использует URL-шаблоны тайлов:

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
  .addTo(map);

Mapbox GL JS использует стили:

mapboxgl.accessToken = 'TOKEN';

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

Следствия миграции

  • отказ от ручного управления tileLayer
  • переход к style-driven рендерингу
  • управление слоями через стиль

Управление слоями

В Leaflet:

map.addLayer(layer);
map.removeLayer(layer);

В Mapbox GL JS:

map.addLayer({
  id: 'roads',
  type: 'line',
  source: 'composite',
  'source-layer': 'road'
});

Удаление:

map.removeLayer('roads');

Важная особенность

Каждый слой в Mapbox GL JS:

  • имеет уникальный id
  • зависит от source
  • описывает стиль отображения

Переход от imperative к declarative модели

Leaflet:

  • императивное добавление объектов
  • прямое управление экземплярами

Mapbox GL JS:

  • декларативное описание состояния карты
  • управление через стиль и источники

Изменение точки мышления:

Leaflet Mapbox GL JS
addMarker() addSource() + addLayer()
update marker setData()
remove marker remove layer/source

Анимации и камера

Leaflet:

map.panTo([51.5, -0.09]);
map.setZoom(10);

Mapbox GL JS:

map.flyTo({
  center: [-0.09, 51.5],
  zoom: 10
});

Дополнительно:

map.easeTo({
  center: [-0.09, 51.5],
  duration: 2000
});

Различие

  • Leaflet: простое перемещение
  • Mapbox GL JS: камера с физикой и анимациями

Кластеризация данных

Leaflet:

  • через плагины (например markercluster)
  • отдельная логика агрегации

Mapbox GL JS:

map.addSource('points', {
  type: 'geojson',
  data: geojson,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

Слои:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'points',
  filter: ['has', 'point_count']
});

Отличие

  • Leaflet: внешние библиотеки
  • Mapbox GL JS: встроенная поддержка

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

Основной фактор перехода:

  • Leaflet ограничен DOM-рендерингом
  • Mapbox GL JS использует GPU (WebGL)

Последствия:

  • тысячи объектов в Leaflet приводят к деградации
  • Mapbox GL JS стабильно держит десятки тысяч точек

Стилизация объектов

Leaflet:

L.circle([51.5, -0.09], {
  color: 'red',
  radius: 500
}).addTo(map);

Mapbox GL JS:

paint: {
  'circle-color': [
    'match',
    ['get', 'type'],
    'A', '#f00',
    'B', '#0f0',
    '#00f'
  ]
}

Сдвиг подхода

  • Leaflet: стили на уровне объекта
  • Mapbox GL JS: стили через выражения

Обработка динамических данных

Leaflet:

marker.setLatLng([lat, lng]);

Mapbox GL JS:

map.getSource('points').setData(newGeoJSON);

Особенность

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

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

Leaflet:

  • canvas overlay
  • custom pane

Mapbox GL JS:

map.addLayer({
  id: 'custom',
  type: 'custom',
  renderingMode: '2d',
  onAdd: function(map, gl) {},
  render: function(gl, matrix) {}
});

Различие

  • Leaflet: DOM/Canvas поверх карты
  • Mapbox GL JS: интеграция в WebGL pipeline

События наведения и интерактивность слоёв

Leaflet:

marker.on('mouseover', fn);

Mapbox GL JS:

map.on('mouseenter', 'points-layer', fn);
map.on('mouseleave', 'points-layer', fn);

Дополнительно:

map.getCanvas().style.cursor = 'pointer';

Итоговые трансформации кода

Leaflet:

L.marker([51.5, -0.09]).addTo(map);

Mapbox GL JS:

map.addSource('point', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: [{
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [-0.09, 51.5]
      }
    }]
  }
});

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