Видимость маркеров в viewport

Google Maps JavaScript API предоставляет доступ к модели отображения карты, где видимая область (viewport) определяется текущими параметрами центра, масштаба и проекции. Работа с маркерами в контексте видимой области требует понимания границ карты, событий изменения viewport и способов эффективной фильтрации объектов, привязанных к географическим координатам.

Видимая область карты представляется объектом LatLngBounds, который описывает прямоугольник в географических координатах. Он определяется юго-западной и северо-восточной точками.

const bounds = map.getBounds();

Объект границ становится доступным только после полной инициализации рендеринга карты. В ранних стадиях жизненного цикла он может возвращать null, что требует привязки логики к событиям изменения состояния карты.

Ключевые характеристики viewport:

  • изменяется при масштабировании (zoom)
  • изменяется при перемещении центра (center)
  • учитывает проекцию Mercator
  • зависит от размеров контейнера карты

Определение видимости маркера

Каждый маркер имеет географическую позицию, получаемую через getPosition():

const position = marker.getPosition();

Проверка видимости выполняется через принадлежность координаты границам viewport:

function isMarkerVisible(marker, map) {
  const bounds = map.getBounds();
  if (!bounds) return false;
  return bounds.contains(marker.getPosition());
}

Метод contains реализует географическую проверку попадания точки внутрь прямоугольника текущего viewport.

События изменения viewport

Для динамической обработки видимости маркеров используется событие bounds_changed. Оно вызывается при любом изменении видимой области.

map.addListener("bounds_changed", () => {
  updateMarkersVisibility();
});

Дополнительно часто используется событие idle, которое возникает после завершения всех анимаций и инерционных перемещений карты:

map.addListener("idle", () => {
  updateMarkersVisibility();
});

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

Фильтрация маркеров по viewport

При большом количестве маркеров прямое управление их отображением становится критичным для производительности. Базовая стратегия заключается в переключении видимости:

function updateMarkersVisibility() {
  const bounds = map.getBounds();
  if (!bounds) return;

  markers.forEach(marker => {
    const visible = bounds.contains(marker.getPosition());
    marker.setVisible(visible);
  });
}

Метод setVisible(false) скрывает маркер без удаления из памяти, что позволяет быстро переключать отображение при изменении viewport.

Управление производительностью при большом количестве объектов

При тысячах маркеров линейная проверка становится узким местом. Сложность O(n) приводит к задержкам при каждом изменении viewport. Используются следующие подходы:

Пространственная индексация

Данные предварительно индексируются в структуру, позволяющую быстро выбирать кандидатов:

  • квадродерево (quadtree)
  • R-tree
  • геохеширование

Пример концептуальной фильтрации через геохеш:

const visibleCells = getVisibleGeoHashCells(map.getBounds());

const candidates = markersByCell.filter(cell =>
  visibleCells.includes(cell.hash)
);

Ограничение частоты обновлений

Для предотвращения избыточных вычислений применяется throttle:

function throttle(fn, delay) {
  let lastCall = 0;
  return function() {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      fn();
    }
  };
}

map.addListener("bounds_changed", throttle(updateMarkersVisibility, 100));

Использование LatLngBounds для массовой фильтрации данных

Фильтрация данных до создания маркеров часто эффективнее, чем управление уже созданными объектами:

function filterDataWithinBounds(data, map) {
  const bounds = map.getBounds();
  if (!bounds) return [];

  return data.filter(item =>
    bounds.contains(new google.maps.LatLng(item.lat, item.lng))
  );
}

Такой подход снижает нагрузку на DOM и рендеринг, особенно при использовании стандартных Marker объектов.

Влияние масштаба на видимость маркеров

Уровень zoom напрямую влияет на плотность отображения объектов. На низком zoom большое количество маркеров перекрывается, что делает фильтрацию по viewport недостаточной без дополнительной логики.

Пример адаптивной фильтрации:

function updateMarkersVisibility() {
  const bounds = map.getBounds();
  const zoom = map.getZoom();
  if (!bounds) return;

  markers.forEach(marker => {
    const position = marker.getPosition();
    const inViewport = bounds.contains(position);

    const visible = inViewport && zoom > 6;
    marker.setVisible(visible);
  });
}

Кластеризация как стратегия управления viewport

При плотном распределении точек используется кластеризация. Библиотека @googlemaps/markerclusterer объединяет маркеры в группы в зависимости от текущего zoom и viewport.

import { MarkerClusterer } from "@googlemaps/markerclusterer";

const clusterer = new MarkerClusterer({ map, markers });

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

Связь viewport и проекции карты

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

const projection = map.getProjection();
const point = projection.fromLatLngToPoint(marker.getPosition());

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

Оптимизация через предварительную группировку данных

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

  • деление мира на сетку фиксированного размера
  • привязка объектов к ячейкам
  • обновление только пересекающихся ячеек viewport

Пример структуры:

const grid = new Map();

function addToGrid(item) {
  const key = getCellKey(item.lat, item.lng);
  if (!grid.has(key)) grid.set(key, []);
  grid.get(key).push(item);
}

Далее фильтрация выполняется только по активным ячейкам viewport.

Поведение маркеров при пересечении границ карты

При изменении долготы карта может циклически повторяться (world wrap). Это влияет на проверку видимости, так как один и тот же объект может быть видим в нескольких экземплярах координатной системы.

В таких случаях проверка bounds.contains может давать неожиданные результаты, и требуется нормализация координат или работа в пиксельной системе проекции.

Разделение логики отображения и данных

Для масштабируемых решений важно отделять:

  • модель данных (географические объекты)
  • слой отображения (маркерные объекты)
  • слой управления viewport

Такое разделение позволяет:

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

Динамическое управление через requestAnimationFrame

При сложных интерактивных сценариях обновление видимости синхронизируется с циклом рендеринга браузера:

let scheduled = false;

function scheduleUpdate() {
  if (scheduled) return;
  scheduled = true;

  requestAnimationFrame(() => {
    updateMarkersVisibility();
    scheduled = false;
  });
}

map.addListener("bounds_changed", scheduleUpdate);

Это снижает нагрузку при частых событиях перемещения карты.

Использование кастомных оверлеев

При переходе от стандартных маркеров к кастомным оверлеям контроль видимости переносится на уровень DOM-элементов. В этом случае viewport становится основным источником истины для отображения.

function updateOverlayVisibility(overlay, map) {
  const bounds = map.getBounds();
  if (!bounds) return;

  const visible = bounds.contains(overlay.position);
  overlay.element.style.display = visible ? "block" : "none";
}

Комплексная модель управления видимостью

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

  • фильтрация по bounding box viewport
  • пространственная индексация
  • кластеризация при низком zoom
  • lazy rendering при выходе за границы viewport
  • ограничение частоты пересчётов

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