Всплывающие подсказки

Базовая модель всплывающих окон

В MapLibre GL JS всплывающие элементы реализуются через класс Popup. Он позволяет отображать произвольный HTML-контент в заданной географической точке или привязке к экранному объекту (например, маркеру или координатам слоя).

Ключевая особенность Popup — его независимость от DOM-карты: окно создаётся как отдельный слой поверх карты и синхронизируется с её проекцией.

Основная инициализация:

const popup = new maplibregl.Popup()
  .setLngLat([37.6173, 55.7558])
  .setHTML("<h3>Москва</h3><p>Столица</p>")
  .addTo(map);

Метод .setLngLat() фиксирует географическую позицию, а .setHTML() задаёт содержимое.


Способы задания содержимого

Popup поддерживает несколько способов наполнения контентом:

HTML через строку

popup.setHTML("<strong>Объект</strong><br/>Описание");

DOM-элемент

const div = document.createElement("div");
div.innerText = "Контент через DOM";

popup.setDOMContent(div);

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


Привязка к маркерам

Наиболее распространённый сценарий — привязка всплывающего окна к маркеру.

const marker = new maplibregl.Marker()
  .setLngLat([37.6173, 55.7558])
  .addTo(map);

const popup = new maplibregl.Popup({ offset: 25 })
  .setHTML("Информация о точке");

marker.setPopup(popup);

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

Параметр offset позволяет смещать окно относительно точки привязки, что предотвращает перекрытие маркера.


Основные параметры Popup

Конструктор Popup принимает объект конфигурации:

new maplibregl.Popup({
  closeButton: true,
  closeOnClick: true,
  closeOnMove: false,
  anchor: "bottom",
  offset: 10,
  className: "custom-popup"
});

closeButton Отображает кнопку закрытия.

closeOnClick Закрывает окно при клике по карте.

closeOnMove Закрывает окно при перемещении карты.

anchor Определяет точку привязки всплывающего окна относительно координаты:

  • top
  • bottom
  • left
  • right
  • top-left
  • top-right
  • bottom-left
  • bottom-right

Привязка к событиям карты

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

Пример отображения информации по клику на объект слоя:

map.on("click", "cities-layer", (e) => {
  const coordinates = e.features[0].geometry.coordinates.slice();
  const name = e.features[0].properties.name;

  new maplibregl.Popup()
    .setLngLat(coordinates)
    .setHTML(`<strong>${name}</strong>`)
    .addTo(map);
});

Метод e.features доступен при использовании интерактивных слоёв с queryRenderedFeatures.


Всплывающие подсказки при наведении

Tooltip-поведение реализуется через событие mouseenter и mouseleave.

let popup;

map.on("mouseenter", "points-layer", (e) => {
  map.getCanvas().style.cursor = "pointer";

  const coordinates = e.features[0].geometry.coordinates.slice();
  const title = e.features[0].properties.title;

  popup = new maplibregl.Popup({
    closeButton: false,
    closeOnClick: false
  })
    .setLngLat(coordinates)
    .setHTML(title)
    .addTo(map);
});

map.on("mouseleave", "points-layer", () => {
  map.getCanvas().style.cursor = "";
  if (popup) popup.remove();
});

Такой подход создаёт эффект «подсказки», не требующий явного клика.


Динамическое обновление содержимого

Popup можно обновлять без пересоздания:

popup.setHTML("Загрузка...");

fetch("/api/info")
  .then(res => res.json())
  .then(data => {
    popup.setHTML(`<pre>${JSON.stringify(data, null, 2)}</pre>`);
  });

Это важно для асинхронных сценариев, когда данные подгружаются после открытия окна.


Программное управление состоянием

Popup предоставляет методы управления жизненным циклом:

popup.addTo(map);     // показать
popup.remove();       // скрыть
popup.isOpen();       // проверка состояния

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


Смещение и позиционирование

Точная настройка позиции достигается через offset.

const popup = new maplibregl.Popup({
  offset: [0, -20]
});

Также допустим объектное задание:

offset: {
  top: [0, 10],
  bottom: [0, -10],
  left: [10, 0],
  right: [-10, 0]
}

Это позволяет учитывать направление стрелки всплывающего окна.


Работа с несколькими окнами

MapLibre не ограничивает количество одновременно открытых popup, однако на практике это может перегружать интерфейс.

Частый паттерн — хранение единственного активного окна:

let activePopup = null;

function showPopup(lngLat, html) {
  if (activePopup) activePopup.remove();

  activePopup = new maplibregl.Popup()
    .setLngLat(lngLat)
    .setHTML(html)
    .addTo(map);
}

Взаимодействие с queryRenderedFeatures

Для получения данных под курсором используется:

map.on("click", (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ["cities-layer"]
  });

  if (!features.length) return;

  const feature = features[0];

  new maplibregl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML(feature.properties.name)
    .addTo(map);
});

Этот механизм обеспечивает точное связывание геометрии и UI-слоя.


HTML-контент и безопасность

Popup поддерживает HTML, что требует контроля входных данных.

Опасный пример:

popup.setHTML(userInput);

Без очистки возможно внедрение скриптов. Безопасный вариант — экранирование:

function escapeHtml(str) {
  return str.replace(/[&<>"']/g, (m) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;"
  }[m]));
}

popup.setHTML(escapeHtml(userInput));

При сложных интерфейсах предпочтительнее setDOMContent, исключающий интерпретацию HTML-строки.


Стилизация всплывающих окон

Popup поддерживает пользовательские классы:

new maplibregl.Popup({
  className: "dark-popup"
});

CSS:

.dark-popup .maplibregl-popup-content {
  background: #1e1e1e;
  color: #ffffff;
  border-radius: 8px;
  padding: 10px;
}

.dark-popup .maplibregl-popup-tip {
  border-top-color: #1e1e1e;
}

Такой подход позволяет полностью интегрировать popup в дизайн приложения.


Закрепление за курсором

Опция trackPointer делает всплывающее окно следящим за курсором:

new maplibregl.Popup({
  closeButton: false,
  closeOnClick: false,
  trackPointer: true
})
  .setHTML("Информация")
  .addTo(map);

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


Производительность и оптимизация

При частых событиях (hover по плотным слоям) создание новых popup на каждый event приводит к деградации производительности.

Оптимальный подход:

  • использовать один экземпляр Popup
  • обновлять содержимое через .setHTML()
  • избегать повторного .addTo(map) без необходимости

Пример оптимизированной логики:

const popup = new maplibregl.Popup({
  closeButton: false,
  closeOnClick: false
});

map.on("mousemove", "layer", (e) => {
  const f = e.features[0];

  popup
    .setLngLat(f.geometry.coordinates)
    .setHTML(f.properties.name);

  if (!popup.isOpen()) popup.addTo(map);
});

Комбинация с кластеризацией

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

map.on("click", "clusters", (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ["clusters"]
  });

  const clusterId = features[0].properties.cluster_id;

  map.getSource("points").getClusterExpansionZoom(
    clusterId,
    (err, zoom) => {
      if (err) return;

      map.easeTo({
        center: features[0].geometry.coordinates,
        zoom
      });
    }
  );
});

Popup здесь может использоваться как промежуточная информация о кластере.


Управление z-index и перекрытием

Popup всегда отображается поверх слоёв карты, но порядок нескольких окон зависит от порядка добавления в DOM. Для контроля используется удаление и повторное добавление или единая точка управления активным состоянием.


Особенности поведения при анимации карты

При flyTo или easeTo popup автоматически пересчитывает позицию, если он привязан к координатам. При сильных анимациях может наблюдаться визуальное смещение, которое корректируется после завершения кадра рендеринга карты.