Попапы на слоях

Попапы (Popup) используются для отображения дополнительной информации поверх карты. Чаще всего они привязываются к объектам слоёв и показываются при клике, наведении курсора или другом событии взаимодействия.

Типичные сценарии использования:

  • отображение атрибутов объекта;
  • вывод карточек организаций;
  • показ статистики по региону;
  • отображение координат точки;
  • открытие мини-интерфейсов прямо на карте.

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


Создание простого попапа

Базовый экземпляр создаётся через конструктор maplibregl.Popup().

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

Основные методы:

Метод Назначение
setLngLat() Установка координат
setHTML() Вставка HTML
setText() Вставка обычного текста
setDOMContent() Передача DOM-элемента
addTo() Добавление на карту
remove() Удаление попапа

После выполнения кода на карте появится окно в заданной точке.


Попап при клике по слою

Наиболее распространённый вариант — открытие окна после выбора объекта слоя.

Предположим, существует слой городов:

map.addLayer({
    id: 'cities',
    type: 'circle',
    source: 'cities-source',
    paint: {
        'circle-radius': 6,
        'circle-color': '#007cbf'
    }
});

Для отображения информации используется обработчик события:

map.on('click', 'cities', (e) => {

    const feature = e.features[0];

    new maplibregl.Popup()
        .setLngLat(feature.geometry.coordinates)
        .setHTML(`
            <h3>${feature.properties.name}</h3>
            <p>Население: ${feature.properties.population}</p>
        `)
        .addTo(map);

});

Механизм работы:

  1. Пользователь нажимает на объект.
  2. MapLibre определяет выбранную сущность.
  3. Из свойства e.features извлекается объект GeoJSON.
  4. Координаты используются для привязки окна.
  5. Атрибуты выводятся в HTML.

Получение данных объекта

Внутри обработчика доступны все свойства GeoJSON-объекта.

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

{
    "type": "Feature",
    "properties": {
        "name": "Москва",
        "population": 13000000,
        "country": "Россия"
    },
    "geometry": {
        "type": "Point",
        "coordinates": [37.6176, 55.7558]
    }
}

Доступ к данным:

map.on('click', 'cities', (e) => {

    const properties = e.features[0].properties;

    console.log(properties.name);
    console.log(properties.population);
    console.log(properties.country);

});

Эти значения обычно выводятся внутри попапа.


Использование setText()

Если отображается только текст, безопаснее использовать setText().

new maplibregl.Popup()
    .setLngLat([37.6176, 55.7558])
    .setText('Москва')
    .addTo(map);

Преимущества:

  • отсутствует интерпретация HTML;
  • исключаются XSS-атаки;
  • автоматически экранируются специальные символы.

Использование setHTML()

Для форматированного содержимого применяется setHTML().

new maplibregl.Popup()
    .setLngLat([37.6176, 55.7558])
    .setHTML(`
        <h2>Москва</h2>
        <p>Население: 13 млн человек</p>
    `)
    .addTo(map);

Внутри HTML можно размещать:

  • заголовки;
  • таблицы;
  • изображения;
  • ссылки;
  • кнопки;
  • формы.

Пример:

new maplibregl.Popup()
    .setLngLat([37.6176, 55.7558])
    .setHTML(`
        <div class="city-card">
            <img src="city.jpg" width="200">
            <h3>Москва</h3>
            <p>Крупнейший город страны</p>
        </div>
    `)
    .addTo(map);

Создание содержимого через DOM

Для сложных интерфейсов удобнее использовать реальные DOM-элементы.

const container = document.createElement('div');

const title = document.createElement('h3');
title.textContent = 'Москва';

const description = document.createElement('p');
description.textContent = 'Столица России';

container.appendChild(title);
container.appendChild(description);

new maplibregl.Popup()
    .setLngLat([37.6176, 55.7558])
    .setDOMContent(container)
    .addTo(map);

Преимущества такого подхода:

  • отсутствие строкового HTML;
  • удобная интеграция с компонентами;
  • лучшая поддержка динамических интерфейсов.

Попап при наведении курсора

Иногда требуется показывать информацию без клика.

let popup;

map.on('mouseenter', 'cities', (e) => {

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

    popup
        .setLngLat(e.features[0].geometry.coordinates)
        .setHTML(`
            <strong>
                ${e.features[0].properties.name}
            </strong>
        `)
        .addTo(map);

});

Закрытие:

map.on('mouseleave', 'cities', () => {

    if (popup) {
        popup.remove();
    }

});

Такой подход часто используется для подсказок (tooltip).


Изменение курсора над объектами

Для улучшения UX обычно меняется курсор.

map.on('mouseenter', 'cities', () => {
    map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'cities', () => {
    map.getCanvas().style.cursor = '';
});

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


Настройка параметров Popup

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

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

Наиболее важные параметры:

Параметр Назначение
closeButton Показывать кнопку закрытия
closeOnClick Закрывать при клике по карте
anchor Положение относительно точки
offset Смещение окна
maxWidth Максимальная ширина
focusAfterOpen Переводить фокус в окно

Управление положением попапа

Параметр anchor задаёт сторону привязки.

new maplibregl.Popup({
    anchor: 'top'
});

Допустимые значения:

top
bottom
left
right
top-left
top-right
bottom-left
bottom-right
center

Пример:

new maplibregl.Popup({
    anchor: 'bottom'
});

Окно будет располагаться над точкой.


Смещение окна

Для тонкой настройки используется offset.

new maplibregl.Popup({
    offset: 20
});

Также допускается объект:

new maplibregl.Popup({
    offset: {
        top: [0, 20],
        bottom: [0, -20]
    }
});

Особенно полезно при работе с крупными маркерами.


Ограничение ширины

По умолчанию ширина ограничена.

Изменение:

new maplibregl.Popup({
    maxWidth: '500px'
});

Либо полное снятие ограничения:

new maplibregl.Popup({
    maxWidth: 'none'
});

Повторное использование одного попапа

Создание нового экземпляра при каждом клике может быть неэффективным.

Лучше использовать один объект.

const popup = new maplibregl.Popup();

map.on('click', 'cities', (e) => {

    const feature = e.features[0];

    popup
        .setLngLat(feature.geometry.coordinates)
        .setHTML(`
            <h3>${feature.properties.name}</h3>
        `)
        .addTo(map);

});

Преимущества:

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

Попап для полигонов

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

map.on('click', 'regions', (e) => {

    new maplibregl.Popup()
        .setLngLat(e.lngLat)
        .setHTML(`
            <h3>${e.features[0].properties.name}</h3>
        `)
        .addTo(map);

});

Использование e.lngLat позволяет показывать окно именно там, где произошло взаимодействие.


Попап для линий

Для линейных объектов используется аналогичный подход.

map.on('click', 'roads', (e) => {

    new maplibregl.Popup()
        .setLngLat(e.lngLat)
        .setHTML(`
            <p>
                Дорога:
                ${e.features[0].properties.name}
            </p>
        `)
        .addTo(map);

});

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


Динамическая загрузка данных

Попап может загружать информацию после открытия.

map.on('click', 'cities', async (e) => {

    const popup = new maplibregl.Popup()
        .setLngLat(e.lngLat)
        .setHTML('Загрузка...')
        .addTo(map);

    const response = await fetch(
        `/api/city/${e.features[0].properties.id}`
    );

    const data = await response.json();

    popup.setHTML(`
        <h3>${data.name}</h3>
        <p>${data.description}</p>
    `);

});

Такой подход позволяет не хранить большой объём данных непосредственно в GeoJSON.


Стилизация попапов

Стандартный внешний вид можно полностью изменить через CSS.

.maplibregl-popup-content {
    padding: 20px;
    border-radius: 12px;
}

.maplibregl-popup-content h3 {
    margin-top: 0;
}

Изменение стрелки:

.maplibregl-popup-tip {
    border-top-color: #ffffff;
}

Изменение тени:

.maplibregl-popup-content {
    box-shadow:
        0 10px 30px rgba(0,0,0,0.2);
}

Назначение собственных CSS-классов

Для индивидуального оформления используется метод addClassName().

const popup = new maplibregl.Popup();

popup.addClassName('city-popup');

CSS:

.city-popup .maplibregl-popup-content {
    background: #f8f8f8;
    border: 2px solid #007cbf;
}

Удаление класса:

popup.removeClassName('city-popup');

Проверка:

popup.toggleClassName('city-popup');

Закрытие программным способом

Удаление окна:

popup.remove();

Проверка существования:

if (popup.isOpen()) {
    popup.remove();
}

Полезно при переключении режимов карты или смене источников данных.


Обработка событий попапа

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

Открытие:

popup.on('open', () => {
    console.log('Попап открыт');
});

Закрытие:

popup.on('close', () => {
    console.log('Попап закрыт');
});

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


Предотвращение накопления нескольких попапов

Частая проблема — появление множества окон после серии кликов.

Решение:

let popup;

map.on('click', 'cities', (e) => {

    if (popup) {
        popup.remove();
    }

    popup = new maplibregl.Popup()
        .setLngLat(e.lngLat)
        .setHTML(`
            <h3>${e.features[0].properties.name}</h3>
        `)
        .addTo(map);

});

На карте всегда остаётся только один активный попап.


Практический пример карточки объекта

const popup = new maplibregl.Popup({
    maxWidth: '350px'
});

map.on('click', 'cities', (e) => {

    const city = e.features[0].properties;

    popup
        .setLngLat(e.lngLat)
        .setHTML(`
            <div class="city-info">
                <h2>${city.name}</h2>

                <table>
                    <tr>
                        <td>Страна</td>
                        <td>${city.country}</td>
                    </tr>

                    <tr>
                        <td>Население</td>
                        <td>${city.population}</td>
                    </tr>
                </table>
            </div>
        `)
        .addTo(map);

});

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