HTML-элементы как маркеры

Базовая модель HTML-маркера

В MapLibre GL JS маркеры могут быть реализованы не только через слой символов (symbol layer), но и через полноценные DOM-элементы. Такой подход используется, когда требуется гибкая визуализация: сложная разметка, произвольная HTML-структура, интерактивные компоненты, формы, изображения или динамические состояния.

HTML-маркер создаётся через maplibregl.Marker, где в качестве содержимого передаётся DOM-узел:

const el = document.createElement('div');
el.className = 'custom-marker';
el.innerHTML = `
  <div class="marker-inner">
    <span class="title">Объект</span>
  </div>
`;

const marker = new maplibregl.Marker({
  element: el
})
  .setLngLat([71.4304, 51.1282])
  .addTo(map);

Ключевой принцип заключается в том, что карта управляет географической привязкой, а браузер — отрисовкой DOM.


Архитектура HTML-маркеров

HTML-маркеры работают поверх WebGL-канваса. Каждый маркер:

  • представляет DOM-элемент, вынесенный в отдельный слой
  • позиционируется через трансформации CSS (transform)
  • синхронизируется с камерой карты (zoom, pitch, bearing)
  • обновляется при каждом кадре или при изменении состояния карты

Позиционирование осуществляется через проекцию координат:

LngLat → Screen coordinates → CSS transform

Это означает, что при каждом перемещении карты выполняется перерасчёт экранной позиции маркера.


Создание маркера с кастомной разметкой

HTML позволяет строить сложные визуальные компоненты:

const wrapper = document.createElement('div');
wrapper.className = 'poi-marker';

const icon = document.createElement('img');
icon.src = '/icons/point.svg';

const label = document.createElement('div');
label.className = 'label';
label.textContent = 'Кафе';

wrapper.appendChild(icon);
wrapper.appendChild(label);

new maplibregl.Marker({ element: wrapper })
  .setLngLat([71.45, 51.15])
  .addTo(map);

Такой подход используется для:

  • точек интереса (POI)
  • пользовательских объектов
  • интерактивных UI-элементов на карте
  • динамических индикаторов состояния

Управление смещением и якорем

Положение HTML-маркера относительно координаты задаётся через anchor и offset.

const marker = new maplibregl.Marker({
  element: el,
  anchor: 'bottom',
  offset: [0, -10]
});

Поддерживаемые якоря:

  • center
  • top
  • bottom
  • left
  • right
  • комбинации (например, top-left)

Смещение позволяет точно позиционировать сложные DOM-структуры относительно географической точки, например:

  • «пины» с хвостиком
  • карточки с тенью
  • всплывающие метки с привязкой к нижнему краю

Обработка событий DOM внутри маркера

HTML-маркеры сохраняют всю модель событий браузера. Это позволяет добавлять интерактивность без дополнительных абстракций:

el.addEventListener('click', (e) => {
  e.stopPropagation();
  console.log('Маркер нажат');
});

Типичные сценарии:

  • открытие карточек объектов
  • переключение состояния (active/inactive)
  • вызов всплывающих окон
  • перетаскивание маркеров

Особенно важно учитывать остановку всплытия событий, чтобы не конфликтовать с обработчиками карты.


Перетаскиваемые HTML-маркеры

Маркер может быть сделан draggable:

const marker = new maplibregl.Marker({
  element: el,
  draggable: true
})
  .setLngLat([71.43, 51.12])
  .addTo(map);

При перемещении происходят события:

marker.on('dragstart', () => {});
marker.on('drag', () => {});
marker.on('dragend', () => {
  const lngLat = marker.getLngLat();
});

Используется для:

  • редактирования геометрии
  • установки пользовательских точек
  • построения маршрутов
  • геокодинга через UI

Производительность и ограничения DOM-маркеров

HTML-маркеры требуют значительных ресурсов по сравнению с WebGL-слоями. Причина — участие браузерного layout engine.

Основные ограничения:

  • большое количество DOM-элементов снижает FPS
  • сложная вложенность HTML увеличивает cost reflow
  • анимации могут конфликтовать с перерисовкой карты

Рекомендации по оптимизации:

  • использовать минимальную DOM-структуру
  • избегать тяжёлых CSS-теней и фильтров
  • применять transform вместо изменения layout-свойств
  • ограничивать количество одновременно видимых маркеров

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

MapLibre GL JS автоматически синхронизирует DOM-маркеры с состоянием камеры:

  • zoom → масштабирование позиции
  • rotate → поворот координат
  • pitch → перспективное смещение
  • pan → пересчёт экранных координат

Внутри используется система обновления, привязанная к render loop карты.


Работа с большим количеством маркеров

При масштабировании данных DOM-подход становится узким местом. Для десятков тысяч объектов HTML-маркеры не подходят.

Используются стратегии:

  • кластеризация точек
  • замена на symbol layer при увеличении плотности
  • виртуализация DOM (рендер только видимых элементов)
  • динамическое создание/удаление маркеров при изменении viewport

Пример условной замены:

if (zoom < 10) {
  useClusterLayer();
} else {
  useHtmlMarkers();
}

Интеграция с React-подобными подходами

HTML-маркеры легко интегрируются с компонентной моделью UI.

Пример концепции:

  • компонент рендерит DOM
  • после mount DOM передаётся в Marker
  • при unmount маркер удаляется
useEffect(() => {
  const el = document.createElement('div');
  el.innerHTML = renderComponent();

  const marker = new maplibregl.Marker({ element: el })
    .setLngLat(coords)
    .addTo(map);

  return () => marker.remove();
}, [coords]);

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

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

Стилизация HTML-маркеров

CSS полностью определяет внешний вид:

.poi-marker {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.15);
  transform: translate3d(0,0,0);
}

.poi-marker .label {
  font-size: 12px;
  white-space: nowrap;
}

Часто применяются:

  • transform: translate3d для ускорения GPU-композита
  • will-change: transform для оптимизации анимаций
  • pointer-events: auto/none для управления взаимодействием

Слои взаимодействия и порядок отрисовки

HTML-маркеры находятся в отдельном DOM-контейнере поверх canvas. Их z-index управляется через:

  • порядок добавления
  • CSS z-index
  • группировку контейнеров

Это позволяет строить многоуровневые интерфейсы:

  • базовая карта
  • WebGL слои данных
  • HTML UI слой
  • модальные элементы поверх карты

Комбинация HTML-маркеров и Popup

HTML-маркеры часто используются совместно с всплывающими окнами:

const popup = new maplibregl.Popup()
  .setLngLat([71.43, 51.12])
  .setHTML('<b>Объект</b>')
  .addTo(map);

Связка позволяет:

  • отображать краткий маркер
  • раскрывать детальную информацию в popup
  • управлять состоянием через события hover/click

Поведение при масштабировании и анимации

При изменении масштаба карты HTML-маркеры:

  • плавно масштабируются вместе с картой
  • пересчитывают позицию каждый frame
  • могут лагать при перегрузке DOM

Особенно заметно:

  • при большом количестве элементов
  • при включённой анимации вращения
  • при сложных CSS эффектах

Оптимизация достигается через упрощение DOM и снижение частоты обновлений.


Управление жизненным циклом маркеров

Каждый HTML-маркер должен быть явно удалён:

marker.remove();

Удаление включает:

  • удаление DOM узла
  • отписку от внутренних событий карты
  • освобождение памяти

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

  • синхронизировать состояние массива маркеров
  • избегать «висячих» DOM элементов
  • контролировать обновления при фильтрации данных