Адаптивный viewport

Понятие viewport в контексте карты

В Leaflet viewport карты представляет собой область отображения карты внутри DOM-контейнера. Это не абстрактное понятие уровня браузера, а конкретная геометрия элемента .leaflet-container, которая определяет:

  • ширину и высоту области отрисовки тайлов;
  • текущий центр карты в координатах широты и долготы;
  • масштаб (zoom level), влияющий на детализацию тайлов;
  • смещение внутренних слоёв при панорамировании.

Любое изменение размеров контейнера напрямую влияет на внутренние расчёты Leaflet. В отличие от статичных изображений, карта не перерисовывается полностью при каждом изменении, а адаптирует существующую тайловую сетку.


Проблема несоответствия размеров контейнера

Leaflet рассчитывает viewport в момент инициализации карты. Если контейнер изменяет размеры после создания экземпляра карты, возникает рассинхронизация:

  • тайлы продолжают рендериться под старый размер;
  • центр карты может смещаться визуально;
  • появляются пустые области или обрезанные тайлы;
  • события pointer/mouse могут не совпадать с визуальной геометрией.

Наиболее частые причины:

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

Механизм обновления размеров в Leaflet

Leaflet не использует автоматический наблюдатель за изменениями DOM. Обновление viewport выполняется вручную через метод пересчёта размеров:

map.invalidateSize();

Этот вызов:

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

Дополнительные параметры:

map.invalidateSize({
  animate: true,
  pan: false
});
  • animate включает плавное смещение карты к новому центру;
  • pan определяет, нужно ли корректировать позицию.

Реакция на изменение окна браузера

Основной сценарий адаптации viewport — обработка события изменения размеров окна:

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

В этом режиме карта синхронизируется с новым размером контейнера, который может зависеть от CSS flex/grid макета.

Однако прямое использование события resize имеет особенности:

  • событие может срабатывать многократно при изменении размера;
  • возможны лишние перерасчёты;
  • на мобильных устройствах resize может вызываться при прокрутке интерфейса.

Использование ResizeObserver

Более точный механизм адаптации основан на ResizeObserver, который отслеживает именно контейнер карты, а не окно браузера:

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

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

observer.observe(container);

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

  • реагирование только на реальные изменения размера карты;
  • отсутствие лишних вызовов при изменении окна, если контейнер фиксирован;
  • корректная работа в сложных layout-сценариях (панели, вкладки, модальные окна).

Адаптация при динамическом отображении контейнера

Частая проблема возникает, когда карта создаётся в скрытом элементе:

  • вкладки (display: none);
  • модальные окна;
  • аккордеоны;
  • динамические панели.

В таких случаях Leaflet не может корректно вычислить размеры, так как offsetWidth и offsetHeight равны нулю.

Корректный сценарий инициализации:

function showMap() {
  mapContainer.style.display = 'block';
  map.invalidateSize();
}

Ключевой момент — вызов пересчёта должен происходить после того, как браузер уже применил стили и отрисовал элемент.


Поведение при flex и grid layout

Современные интерфейсы часто используют flexbox или CSS grid. В этих случаях контейнер карты может изменять размеры без явного события resize окна.

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

  • изменение соседних блоков влияет на доступное пространство карты;
  • перераспределение пространства происходит асинхронно;
  • Leaflet не получает уведомлений о таких изменениях.

Типичный паттерн:

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

.sidebar {
  width: 300px;
}

.map {
  flex: 1;
}

При сворачивании sidebar карта изменяет размер, и требуется явный вызов пересчёта.


Fullscreen и адаптивный режим

При переходе в полноэкранный режим изменяется viewport браузера, но не всегда корректно обновляется контейнер карты.

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

Особенности поведения:

  • на некоторых браузерах изменение fullscreen происходит с задержкой;
  • возможны промежуточные состояния с некорректной геометрией;
  • требуется вызов после завершения transition.

Адаптация под мобильные устройства

На мобильных устройствах viewport усложняется из-за:

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

Обработка ориентации:

window.addEventListener('orientationchange', () => {
  setTimeout(() => {
    map.invalidateSize();
  }, 200);
});

Задержка позволяет системе завершить перерасчёт layout до обновления карты.


Safe area и современные экраны

На устройствах с вырезами и нестандартными границами (notch) viewport может не совпадать с визуальной областью.

CSS-адаптация:

.map {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

Leaflet учитывает только размеры контейнера, поэтому любые safe-area корректировки должны быть отражены в DOM перед вызовом invalidateSize.


Производительность при частых изменениях viewport

При интенсивных изменениях размера (drag панели, анимации интерфейса) частые вызовы пересчёта могут влиять на производительность.

Типичный способ оптимизации:

let resizeTimeout;

window.addEventListener('resize', () => {
  clearTimeout(resizeTimeout);
  resizeTimeout = setTimeout(() => {
    map.invalidateSize();
  }, 100);
});

Такой подход снижает нагрузку за счёт батчинга операций.


Сохранение состояния карты при ресайзе

При изменении viewport важно сохранять:

  • центр карты;
  • текущий zoom;
  • активные слои и оверлеи.

Leaflet по умолчанию сохраняет состояние, но при сложных сценариях (например, ручной reset view) может потребоваться явное управление:

const center = map.getCenter();
const zoom = map.getZoom();

map.invalidateSize();
map.setView(center, zoom);

Взаимодействие viewport с tile loading

Изменение размеров влияет на:

  • количество загружаемых тайлов;
  • область prefetch за границами экрана;
  • приоритет загрузки слоёв.

После invalidateSize Leaflet пересчитывает tile grid, что может приводить к кратковременной загрузке новых сегментов карты, особенно при резком увеличении контейнера.