Адаптивность карты

Адаптивность отображения в OpenLayers строится вокруг синхронизации размеров DOM-контейнера и внутреннего viewport карты. Любое изменение геометрии блока, в котором размещена карта, требует уведомления экземпляра Map, иначе визуальный слой останется в устаревших координатах пиксельного пространства.

Базовый принцип пересчёта размеров

Карта в OpenLayers опирается на фиксированную привязку к контейнеру. При изменении размеров контейнера необходимо явно вызывать пересчёт:

map.updateSize();

Этот метод инициирует перерасчёт внутренних параметров рендера, включая:

  • размер viewport;
  • матрицу преобразования координат;
  • область видимости (extent);
  • положение слоёв.

Без вызова updateSize() карта визуально «ломается»: тайлы остаются в прежнем положении, появляются пустые зоны или смещения.

Контейнер как основа адаптивности

Корректная адаптивность начинается не с API карты, а с CSS-структуры:

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

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

Ключевой момент — отсутствие неопределённой высоты. OpenLayers не вычисляет высоту автоматически, поэтому height: 100% без заданной высоты родителей приводит к нулевому viewport.

Для более сложных интерфейсов часто используется flex-раскладка:

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

.sidebar {
  width: 300px;
}

.map {
  flex: 1;
}

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

Простейший способ обеспечить адаптивность — подписка на событие resize:

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

Однако этот подход имеет ограничение: он реагирует только на изменение окна браузера, игнорируя изменения DOM-структуры (например, открытие сайдбара или изменение панели инструментов).

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

Более точный механизм — ResizeObserver, который отслеживает изменения конкретного контейнера:

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

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

observer.observe(target);

Этот подход корректно работает при:

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

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

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

Рекомендуемая стратегия:

  • использовать 100dvh вместо 100vh;
  • учитывать safe-area inset;
  • избегать фиксированных высот без пересчёта.
#map {
  height: 100dvh;
}

При необходимости дополнительной стабильности применяется принудительный пересчёт:

window.addEventListener('orientationchange', () => {
  setTimeout(() => map.updateSize(), 300);
});

Задержка компенсирует анимацию изменения viewport в мобильных браузерах.

Влияние pixel ratio на отображение

OpenLayers автоматически учитывает devicePixelRatio, что влияет на чёткость тайлов и векторного рендера. При адаптивной верстке важно учитывать, что изменение масштабирования интерфейса может сопровождаться перерасчётом canvas.

Для стабилизации рендеринга используется:

const map = new Map({
  target: 'map',
  pixelRatio: window.devicePixelRatio
});

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

Динамическое изменение контейнера

При изменении layout (например, открытие боковой панели) важно синхронизировать состояние карты:

function toggleSidebar() {
  document.body.classList.toggle('sidebar-open');
  requestAnimationFrame(() => {
    map.updateSize();
  });
}

Использование requestAnimationFrame позволяет дождаться завершения перерасчёта layout перед обновлением viewport.

Адаптация контролов интерфейса

Контролы карты (Zoom, Rotate, Attribution) должны оставаться доступными при любом размере экрана. На узких устройствах их расположение часто меняется:

import { defaults as defaultControls } from 'ol/control';

const map = new Map({
  controls: defaultControls({
    attribution: true,
    zoom: true,
    rotate: false
  }),
  target: 'map'
});

Для мобильной версии контролы могут быть переупорядочены через CSS:

.ol-control {
  transform: scale(0.9);
}

Работа с fullscreen-режимом

При переходе в полноэкранный режим необходимо учитывать задержку изменения размеров:

document.addEventListener('fullscreenchange', () => {
  setTimeout(() => map.updateSize(), 100);
});

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

Debounce при частых изменениях

При активной анимации интерфейса или частых изменениях DOM полезно ограничивать частоту вызова пересчёта:

function debounce(fn, delay) {
  let t;
  return () => {
    clearTimeout(t);
    t = setTimeout(fn, delay);
  };
}

const safeUpdate = debounce(() => map.updateSize(), 100);

window.addEventListener('resize', safeUpdate);

Это снижает нагрузку на рендерер и предотвращает избыточные пересчёты матриц.

Синхронизация с анимациями интерфейса

Если карта встроена в интерфейс с плавными переходами (например, CSS transition для панели), обновление размера должно происходить после завершения анимации:

sidebar.addEventListener('transitionend', () => {
  map.updateSize();
});

Без этого карта часто фиксирует промежуточное состояние контейнера.

Адаптивность слоёв и источников данных

При изменении масштаба экрана может потребоваться адаптация источников данных:

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

Пример динамической смены tile layer:

layer.setSource(new XYZ({
  url: isMobile
    ? 'https://tiles-mobile.example/{z}/{x}/{y}.png'
    : 'https://tiles-desktop.example/{z}/{x}/{y}.png'
}));

Геометрическая стабильность view

View карты должен сохранять согласованность при любых изменениях размеров:

const view = new View({
  center: [0, 0],
  zoom: 2,
  constrainResolution: true
});

Параметр constrainResolution предотвращает появление дробных уровней масштаба при резких изменениях контейнера.

Особенности высокоплотных экранов

На дисплеях с высокой плотностью пикселей важно избегать двойного масштабирования:

  • CSS масштабирует контейнер;
  • Canvas масштабируется через pixel ratio.

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

Практика устойчивой адаптивной архитектуры

Стабильная схема адаптивной карты включает три уровня:

  1. CSS-уровень

    • flex/grid layout
    • корректные высоты
    • отсутствие нулевых контейнеров
  2. DOM-уровень

    • ResizeObserver
    • контроль изменений структуры
    • синхронизация анимаций
  3. OpenLayers-уровень

    • map.updateSize()
    • управление View
    • корректировка источников данных

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