Адаптивный размер

Базовый принцип работы размера карты

В Mapbox GL JS размер карты напрямую зависит от размеров DOM-контейнера, в который она инициализирована. Библиотека не управляет версткой самостоятельно — она лишь считывает вычисленные CSS-значения ширины и высоты и использует их для расчета viewport WebGL-контекста.

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

Основной метод для этого:

map.resize();

Метод пересчитывает размеры canvas и корректирует внутренние матрицы проекции.


Инициализация карты с гибким контейнером

Правильная адаптация начинается на уровне CSS. Контейнер должен иметь предсказуемую модель размеров, иначе карта будет рассчитывать некорректный viewport.

Типичная структура:

<div id="map"></div>
html, body {
  margin: 0;
  height: 100%;
}

#map {
  width: 100%;
  height: 100vh;
}

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

Инициализация:

mapboxgl.accessToken = 'TOKEN';

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

Изменение размера окна и необходимость resize()

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

Стандартный подход:

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

Однако этот вариант не учитывает случаи, когда изменение размеров происходит не из-за окна, а из-за layout-изменений внутри приложения.


Динамические интерфейсы и проблема контейнеров

В SPA и интерфейсах с панелями часто встречается ситуация, когда карта находится внутри flex или grid layout, где размеры могут изменяться без события resize окна.

Пример layout:

.wrapper {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: 300px;
}

.map-container {
  flex: 1;
  position: relative;
}

#map {
  width: 100%;
  height: 100%;
}

Если sidebar изменяет ширину (например, скрывается), контейнер карты изменяется, но событие window resize не срабатывает.


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

Современный способ отслеживания изменений размеров контейнера — ResizeObserver.

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

const observer = new ResizeObserver(() => {
  map.resize();
});

observer.observe(container);

Этот подход реагирует на любые изменения размеров:

  • изменение flex-раскладки
  • появление/исчезновение панелей
  • динамическая загрузка контента
  • изменение CSS-классов

Отложенный resize и проблемы рендеринга

При некоторых изменениях DOM браузер сначала перерасчитывает layout, а затем применяет стили. В таких случаях вызов map.resize() может произойти до завершения перерасчета.

Решение — отложенный вызов:

const resizeMap = () => {
  requestAnimationFrame(() => {
    map.resize();
  });
};

Или двойной RAF для сложных интерфейсов:

const resizeMap = () => {
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      map.resize();
    });
  });
};

Адаптация при скрытии и показе контейнера

Особенно критичный случай — карта внутри элемента с display: none.

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

sidebar.classList.add('hidden');

// позже
sidebar.classList.remove('hidden');
map.resize();

Если этого не сделать, карта может остаться с нулевыми или устаревшими размерами canvas.


Работа с вкладками и lazy rendering

В интерфейсах с вкладками карта часто инициализируется в неактивной вкладке, где контейнер имеет нулевые размеры. Это приводит к тому, что WebGL canvas создается с нулевой геометрией.

Корректная стратегия:

  1. инициализация карты
  2. ожидание отображения контейнера
  3. вызов resize
tabButtons.forEach(btn => {
  btn.addEventListener('click', () => {
    showTab('mapTab');
    map.resize();
  });
});

Полноэкранный режим и изменение viewport

При переходе в fullscreen изменяется размер контейнера и пересчитываются системные параметры viewport.

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

Это особенно важно, так как fullscreen меняет не только CSS-геометрию, но и внутренние размеры device pixel ratio.


High DPI и адаптация к pixel ratio

Mapbox GL JS автоматически учитывает devicePixelRatio, но изменение масштаба системы (например, drag окна между мониторами с разной плотностью пикселей) может требовать пересчета.

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

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


Минимизация количества resize вызовов

Частые вызовы map.resize() могут приводить к лишним перерасчетам WebGL сцены. При сложных интерфейсах важно агрегировать изменения.

Пример батчинга:

let resizeScheduled = false;

function scheduleResize() {
  if (resizeScheduled) return;

  resizeScheduled = true;

  requestAnimationFrame(() => {
    map.resize();
    resizeScheduled = false;
  });
}

Этот подход особенно полезен при массовых DOM-изменениях.


Изменение размера через CSS transitions

Если контейнер изменяет размер плавно (например, анимация sidebar), требуется учитывать промежуточные состояния.

.sidebar {
  transition: width 0.3s ease;
}

В этом случае ResizeObserver будет вызывать map.resize() многократно во время анимации. Это может быть нежелательно.

Оптимизация — debounce:

let timeout;

const observer = new ResizeObserver(() => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    map.resize();
  }, 50);
});

Работа с абсолютным позиционированием

Если карта используется как фон слоя интерфейса:

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

Ключевым становится контроль высоты через top/bottom, а не height. Это позволяет карте автоматически подстраиваться под родительский контейнер без дополнительных вычислений.


Сложные сценарии вложенных контейнеров

В многоуровневых интерфейсах (панели, модальные окна, split-view) карта часто оказывается в цепочке контейнеров, каждый из которых может влиять на итоговый размер.

Типичная проблема — один из предков имеет overflow: hidden и изменяемую высоту. В таких случаях ResizeObserver должен наблюдать именно тот элемент, который фактически определяет viewport карты, а не внешний wrapper.


Принудительная синхронизация состояния карты

В некоторых случаях требуется синхронизировать размер не только canvas, но и логическое состояние карты (например, после восстановления layout).

function syncMapLayout() {
  map.resize();
  map.triggerRepaint();
}

triggerRepaint() используется для принудительного перерисовывания сцены после изменения геометрии, если изменения происходят в нестандартных циклах рендеринга.


Поведение при инициализации в скрытом состоянии

Если карта создается внутри скрытого контейнера, WebGL контекст может быть инициализирован с нулевыми размерами. Это приводит к некорректной отрисовке тайлов и слоев.

Стратегия корректной инициализации:

  • либо отложенная инициализация до отображения контейнера
  • либо немедленный вызов map.resize() после показа
const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/light-v11' });

showContainer();
map.resize();

Итоговые принципы адаптивного размера

  • размер карты полностью зависит от размеров DOM-контейнера
  • любые изменения layout требуют синхронизации через map.resize()
  • window resize — лишь частный случай изменения геометрии
  • современные интерфейсы требуют ResizeObserver как базовый механизм отслеживания
  • скрытые, анимируемые и динамические контейнеры требуют явного контроля жизненного цикла карты