Выделение объектов на карте в MapLibre GL JS опирается на комбинацию трёх механизмов: получение объектов под курсором, управление состоянием feature-state и динамическое изменение стилей слоёв. В зависимости от типа источника данных (GeoJSON, векторные тайлы, кластеризация) подход к выделению отличается, однако общая архитектура остаётся одинаковой — идентификация объекта и применение визуального состояния.
Основной способ определить, какой объект был выбран, — использование
метода queryRenderedFeatures. Он возвращает список
объектов, отрисованных в текущем viewport, соответствующих заданным
фильтрам.
map.on('click', 'cities-layer', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
const feature = features[0];
});
Метод работает только с уже отрисованными данными, поэтому он зависит от текущего масштаба и видимой области. При наличии нескольких перекрывающихся объектов возвращается массив, где порядок определяется приоритетом слоёв и порядком рендеринга.
Для более общего сценария без привязки к конкретному слою используется:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point);
});
Ключевым условием корректного выделения является наличие стабильного
идентификатора. Векторные источники должны предоставлять
id, либо он задаётся через promoteId.
map.addSource('cities', {
type: 'vector',
url: 'https://example.com/tiles.json',
promoteId: 'id'
});
Для GeoJSON-источников идентификатор обычно присутствует в свойствах:
map.addSource('places', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{ type: 'Feature', id: 1, properties: { name: 'A' }, geometry: {...} }
]
}
});
Без стабильного идентификатора невозможно корректно управлять состоянием выделения при перерисовке слоя.
Механизм feature-state является основным способом
хранения динамического состояния объектов без изменения исходных данных.
Состояние привязывается к конкретному feature через его
идентификатор.
map.setFeatureState(
{ source: 'places', id: 1 },
{ selected: true }
);
Сброс состояния:
map.setFeatureState(
{ source: 'places', id: 1 },
{ selected: false }
);
Удаление состояния полностью:
map.removeFeatureState(
{ source: 'places', id: 1 }
);
После установки состояния необходимо связать его с визуальным
представлением слоя через выражения paint или
layout.
Пример изменения цвета при выделении:
map.addLayer({
id: 'places-layer',
type: 'circle',
source: 'places',
paint: {
'circle-radius': 6,
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ff0000',
'#3388ff'
]
}
});
Аналогичный подход применяется для линий:
'line-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ff0000',
'#2a2a2a'
]
И для полигонов:
'fill-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ffcc00',
'#0066cc'
]
Типичный сценарий — выделение только одного объекта. Для этого
необходимо хранить текущий выбранный id и сбрасывать
предыдущее состояние.
let selectedId = null;
map.on('click', 'places-layer', (e) => {
const feature = e.features[0];
if (selectedId !== null) {
map.setFeatureState(
{ source: 'places', id: selectedId },
{ selected: false }
);
}
selectedId = feature.id;
map.setFeatureState(
{ source: 'places', id: selectedId },
{ selected: true }
);
});
Такая схема предотвращает накопление активных состояний и упрощает синхронизацию UI.
Для множественного выбора используется структура данных, например
Set, содержащая идентификаторы активных объектов.
const selectedIds = new Set();
map.on('click', 'places-layer', (e) => {
const id = e.features[0].id;
if (selectedIds.has(id)) {
selectedIds.delete(id);
map.setFeatureState({ source: 'places', id }, { selected: false });
} else {
selectedIds.add(id);
map.setFeatureState({ source: 'places', id }, { selected: true });
}
});
Такой подход применяется для сценариев выбора нескольких объектов, например при построении фильтров или групповых операций.
Выделение при наведении реализуется через событие
mousemove. Для оптимизации используется сброс предыдущего
состояния.
let hoveredId = null;
map.on('mousemove', 'places-layer', (e) => {
const feature = e.features[0];
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'places', id: hoveredId },
{ hover: false }
);
}
hoveredId = feature.id;
map.setFeatureState(
{ source: 'places', id: hoveredId },
{ hover: true }
);
});
И соответствующая стилизация:
'circle-radius': [
'case',
['boolean', ['feature-state', 'hover'], false],
10,
6
]
При выходе курсора за пределы слоя необходимо сбрасывать hover-состояние:
map.on('mouseleave', 'places-layer', () => {
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'places', id: hoveredId },
{ hover: false }
);
}
hoveredId = null;
});
Вместо feature-state иногда применяется изменение filter
слоя. Этот подход используется реже из-за высокой стоимости
перерисовки.
map.setFilter('places-layer', [
'!=',
['id'],
selectedId
]);
Или наоборот:
map.setFilter('places-layer', [
'==',
['id'],
selectedId
]);
Данный метод эффективен только при небольших наборах данных и простых визуальных сценариях.
При использовании кластеризации GeoJSON выделение требует разворачивания кластера в дочерние объекты.
map.on('click', 'clusters', (e) => {
const clusterId = e.features[0].properties.cluster_id;
map.getSource('places').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
map.easeTo({
center: e.features[0].geometry.coordinates,
zoom
});
}
);
});
После раскрытия кластера дальнейшее выделение выполняется уже на уровне отдельных точек.
Состояние выделения часто дублируется в внешнем состоянии приложения. Это позволяет синхронизировать карту с панелями информации, списками и фильтрами.
const state = {
selectedFeature: null
};
map.on('click', 'places-layer', (e) => {
const feature = e.features[0];
state.selectedFeature = feature;
});
Такой подход предотвращает зависимость логики от карты как единственного источника истины.
feature-state хранится отдельно от геометрии и не
требует пересборки источников, однако чрезмерное количество обновлений
состояния может приводить к деградации производительности. Особенно это
заметно при массовом обновлении сотен объектов в цикле.
Оптимизация заключается в ограничении числа операций
setFeatureState и группировке изменений на уровне логики
приложения.
При изменении стилей или источников состояние
feature-state сохраняется, если идентификаторы остаются
стабильными. Однако при полной замене source данные состояния
сбрасываются, что требует повторного восстановления выделения.
Для повышения визуальной обратной связи обычно синхронизируется стиль курсора:
map.on('mouseenter', 'places-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'places-layer', () => {
map.getCanvas().style.cursor = '';
});
Такой механизм дополняет логическое выделение и делает взаимодействие более предсказуемым.
Часто одновременно используются несколько состояний:
selected, hover, active,
disabled. Они комбинируются через выражения
case.
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ff0000',
['boolean', ['feature-state', 'hover'], false],
'#ffaa00',
'#3388ff'
]
Порядок условий определяет приоритет визуальных состояний и должен быть согласован с логикой обработки событий.