Позиционирование окон

Основы системы координат и экранного пространства

Позиционирование всплывающих окон в Google Maps JavaScript API опирается на две системы координат: географическую (широта и долгота) и экранную (пиксели относительно контейнера карты). Любое визуальное окно, связанное с точкой на карте, в конечном итоге преобразуется из LatLng в координаты DOM-слоя карты.

Ключевым элементом этой системы является проекция карты, предоставляемая через интерфейс Projection. Она отвечает за преобразование географических координат в пиксельные значения относительно текущего состояния карты (центр, масштаб, поворот).


InfoWindow и базовое привязывание к координате

InfoWindow является стандартным механизмом отображения всплывающих окон. Его позиционирование напрямую связано с объектом LatLng, к которому оно привязывается.

const map = new google.maps.Map(document.getElementById("map"), {
  center: { lat: 40.73061, lng: -73.935242 },
  zoom: 10
});

const marker = new google.maps.Marker({
  position: { lat: 40.73061, lng: -73.935242 },
  map: map
});

const infoWindow = new google.maps.InfoWindow({
  content: "<div>Точка интереса</div>"
});

marker.addListener("click", () => {
  infoWindow.open({
    anchor: marker,
    map: map
  });
});

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


Якоря (anchor) и смещение пикселей

Позиционирование окон через anchor предполагает наличие точки привязки. У маркеров эта точка определяется свойством anchorPoint, которое задаёт место, относительно которого рассчитывается всплывающее окно.

Для более точной настройки используется pixelOffset, позволяющий смещать окно относительно базовой точки.

const infoWindow = new google.maps.InfoWindow({
  content: "Смещение окна",
  pixelOffset: new google.maps.Size(0, -30)
});

pixelOffset принимает объект Size(width, height), где:

  • width смещает окно по горизонтали
  • height смещает окно по вертикали

Отрицательное значение по вертикали часто используется для подъёма окна над маркером.


Преобразование координат через Projection

Для ручного позиционирования используется слой проекции карты. Он доступен через OverlayView.

class CustomOverlay extends google.maps.OverlayView {
  onAdd() {
    this.div = document.createElement("div");
    this.div.style.position = "absolute";
    this.getPanes().overlayMouseTarget.appendChild(this.div);
  }

  draw() {
    const projection = this.getProjection();
    const position = projection.fromLatLngToDivPixel(this.latlng);

    this.div.style.left = position.x + "px";
    this.div.style.top = position.y + "px";
  }

  onRemove() {
    this.div.parentNode.removeChild(this.div);
  }
}

Методы преобразования:

  • fromLatLngToDivPixel(latLng) — перевод в пиксели
  • fromDivPixelToLatLng(pixel) — обратное преобразование

Эти функции учитывают текущий zoom и центр карты, что позволяет синхронизировать DOM-элементы с картой.


OverlayView как основа кастомного позиционирования

OverlayView предоставляет полный контроль над размещением HTML-элементов поверх карты. Это используется для реализации кастомных окон, тултипов и интерактивных попапов.

Жизненный цикл:

  • onAdd() — добавление DOM-элемента
  • draw() — вычисление позиции
  • onRemove() — очистка ресурсов

Ключевой момент заключается в том, что draw() вызывается при каждом изменении состояния карты: zoom, pan, resize.


Управление видимой областью и автоматическое смещение

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

  • panTo(latLng)
  • panBy(x, y)
  • fitBounds(bounds)
map.panTo(marker.getPosition());

При сложных интерфейсах учитывается высота окна, чтобы избежать его обрезания верхней границей viewport.


Учет масштаба и динамическое перераспределение

При изменении zoom пиксельные координаты пересчитываются автоматически, однако пользовательские окна требуют явного обновления позиции. В OverlayView это решается повторным вычислением в draw().

Особое внимание уделяется:

  • изменению размеров контейнера карты
  • изменению devicePixelRatio
  • инерционным анимациям pan/zoom

Работа с границами карты и краевыми случаями

При позиционировании окон рядом с границами карты возникает проблема выхода за пределы viewport. Встроенные окна автоматически применяют логики “clamping” и смещения.

В кастомных реализациях требуется ручная проверка:

  • положение относительно краёв контейнера
  • доступное пространство сверху и снизу
  • потенциальное смещение по оси X при близости к краю

Пример корректировки:

if (position.y < 50) {
  this.div.style.top = "50px";
}

Слои отображения и порядок наложения

Каждое окно в Google Maps размещается в одном из панов (panes):

  • overlayLayer — базовые оверлеи
  • overlayMouseTarget — интерактивные элементы
  • floatPane — всплывающие окна и InfoWindow

Выбор слоя определяет z-index и поведение при наведении мыши.

this.getPanes().floatPane.appendChild(this.div);

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


Синхронизация с событиями карты

Позиционирование окон зависит от событий:

  • bounds_changed
  • zoom_changed
  • center_changed
  • resize

Для кастомных окон требуется привязка обновления позиции к этим событиям:

google.maps.event.addListener(map, "zoom_changed", () => {
  overlay.draw();
});

Особенности при работе с несколькими окнами

При одновременном отображении нескольких окон важно учитывать:

  • конкуренцию за центрирование карты
  • перекрытие окон друг другом
  • управление z-index через DOM или panes

Часто применяется стратегия “активного окна”, при которой только одно окно имеет приоритетную позицию, остальные смещаются или скрываются.


Практика вычисления смещения относительно маркера

Маркер в Google Maps имеет фиксированную точку привязки (обычно нижний центр). Это влияет на позиционирование окна:

  • окно должно учитывать высоту маркера
  • требуется вертикальный offset
  • возможна компенсация тени маркера
const infoWindow = new google.maps.InfoWindow({
  pixelOffset: new google.maps.Size(0, -40)
});

Работа с адаптивной версткой карты

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

Типичный механизм:

  • слушатель resize на контейнере
  • вызов google.maps.event.trigger(map, "resize")
  • повторное центрирование или обновление overlay

Смещение окна при изменении масштаба

При резком изменении zoom положение окна может визуально “прыгать”. Это связано с округлением пиксельных значений и изменением масштаба тайлов.

Для стабилизации применяется:

  • сглаживание через CSS transition
  • пересчет позиции с задержкой
  • фиксация anchor в географических координатах без промежуточных пересчетов DOM

Использование абсолютного позиционирования в DOM-слое карты

Все кастомные окна в конечном счете используют position: absolute внутри слоев карты. Это требует строгого контроля:

  • отсутствие конфликтов с transform контейнеров
  • учет overflow скрытия
  • корректная работа с pointer-events
.custom-popup {
  position: absolute;
  transform: translate(-50%, -100%);
}

Такой подход позволяет центрировать окно относительно точки привязки без дополнительных вычислений.


Проблемы долготы на границах мира

При работе с координатами, близкими к ±180° долготы, возникает эффект “перепрыгивания” позиции. Это связано с циклической природой карты мира.

Решение заключается в:

  • нормализации долготы
  • использовании метода wrapLongitude
  • проверке расстояния между точками в экранных координатах, а не географических

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

Позиционирование окон в Google Maps JavaScript API строится на сочетании:

  • географических координат (LatLng)
  • пиксельной проекции (Projection)
  • DOM-слоя overlay panes
  • событийной модели карты

Корректная работа достигается только при синхронном учете всех уровней трансформации: от координат мира до абсолютного положения элемента в DOM.