Выбор объектов на карте в Mapbox GL JS строится вокруг работы с векторными слоями и интерактивными событиями карты. Основная задача — определить, какие географические объекты (точки, линии, полигоны) попадают под взаимодействие пользователя, и отразить их состояние через визуальные изменения слоя.
Ключевой механизм взаимодействия — получение объектов из
отрисованного кадра с помощью queryRenderedFeatures. В
отличие от серверных запросов или анализа исходных данных GeoJSON, этот
метод работает с уже отрендеренными тайлами, что делает его быстрым и
пригодным для интерактивного выделения.
Основной сценарий простого выбора начинается с обработки события
click или mousemove. При клике координаты
экрана преобразуются в список объектов слоя:
map.on('click', 'cities-layer', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
console.log(features);
});
Параметр e.point содержит пиксельные координаты события.
Опция layers ограничивает выборку конкретным слоем, что
критично при наличии перекрывающихся объектов.
queryRenderedFeatures возвращает массив объектов, каждый
из которых содержит:
id (если задан в данных)geometrypropertieslayerЭти данные используются как основа для дальнейшего выделения.
Простейшая логика выбора предполагает хранение одного активного объекта. При каждом клике предыдущий выбор сбрасывается, новый устанавливается как активный.
let selectedFeatureId = null;
map.on('click', 'cities-layer', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const feature = features[0];
selectedFeatureId = feature.id;
});
Такой подход требует, чтобы у каждого объекта в источнике данных был
уникальный id. Без него невозможна стабильная идентификация
выбранного элемента.
Один из наиболее прямолинейных способов визуального выделения — использование фильтра слоя. Слой дублируется: один отображает все объекты, второй — только выбранный.
map.addLayer({
id: 'cities-layer',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': 6,
'circle-color': '#3b82f6'
}
});
map.addLayer({
id: 'cities-layer-selected',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': 8,
'circle-color': '#ef4444'
},
filter: ['==', 'id', '']
});
Обновление состояния выполняется через изменение фильтра:
map.setFilter('cities-layer-selected', [
'==',
'id',
selectedFeatureId
]);
Данный подход прост, но требует дополнительного слоя и дублирования визуальной логики.
Более гибкий и производительный способ основан на
feature-state. Он позволяет хранить состояние объекта
отдельно от его данных.
При клике состояние обновляется:
map.on('click', 'cities-layer', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const feature = features[0];
if (selectedFeatureId !== null) {
map.setFeatureState(
{ source: 'cities', id: selectedFeatureId },
{ selected: false }
);
}
selectedFeatureId = feature.id;
map.setFeatureState(
{ source: 'cities', id: selectedFeatureId },
{ selected: true }
);
});
Визуальная часть слоя реагирует на состояние:
map.addLayer({
id: 'cities-layer',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': [
'case',
['boolean', ['feature-state', 'selected'], false],
8,
5
],
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ef4444',
'#3b82f6'
]
}
});
Такой подход исключает необходимость дублирования слоя и позволяет масштабировать интерактивность на большие наборы данных.
Для корректной работы feature-state необходимо
соблюдение условия: источник данных должен поддерживать идентификаторы
объектов.
GeoJSON-источник должен быть создан следующим образом:
map.addSource('cities', {
type: 'geojson',
data: citiesGeojson,
generateId: true
});
Параметр generateId: true автоматически назначает
уникальные идентификаторы, если они отсутствуют в исходных данных.
Без стабильного id невозможно гарантировать корректное
обновление состояния, особенно при перерисовке тайлов.
При наличии нескольких слоёв или перекрывающихся геометрий
queryRenderedFeatures может возвращать несколько объектов.
Порядок зависит от z-index слоёв и порядка их добавления.
Фильтрация до первого объекта:
const feature = features[0];
может быть недостаточной. Более точный контроль достигается через приоритет слоя:
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
Или через дополнительную сортировку по свойствам:
features.sort((a, b) => b.properties.importance - a.properties.importance);
Сброс состояния выполняется явным удалением feature-state:
if (selectedFeatureId !== null) {
map.setFeatureState(
{ source: 'cities', id: selectedFeatureId },
{ selected: false }
);
selectedFeatureId = null;
}
Дополнительно можно использовать очистку через
removeFeatureState, если требуется полное удаление
состояния:
map.removeFeatureState({
source: 'cities',
id: selectedFeatureId
});
При клике по пустой области карта не возвращает features, что требует явной обработки:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) {
if (selectedFeatureId !== null) {
map.setFeatureState(
{ source: 'cities', id: selectedFeatureId },
{ selected: false }
);
selectedFeatureId = null;
}
}
});
Это обеспечивает консистентное поведение интерфейса выбора.
При необходимости учитывать несколько типов объектов выборка расширяется:
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer', 'villages-layer']
});
В этом случае логика выбора должна учитывать тип объекта:
const feature = features.find(f => f.layer.id === 'cities-layer');
Или через приоритет:
const priority = {
'cities-layer': 2,
'villages-layer': 1
};
features.sort((a, b) => priority[b.layer.id] - priority[a.layer.id]);
Интерактивный выбор в Mapbox GL JS зависит от частоты событий и сложности слоёв. При высокой плотности данных возможны следующие узкие места:
queryRenderedFeaturespaintfeature-stateОптимизация достигается через:
layers в запросеИспользование filter:
Использование feature-state:
Использование queryRenderedFeatures:
Полный цикл простого выбора включает три этапа:
queryRenderedFeaturesfeature-state или
filterlet selectedFeatureId = null;
map.on('click', 'cities-layer', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const feature = features[0];
if (selectedFeatureId !== null) {
map.setFeatureState(
{ source: 'cities', id: selectedFeatureId },
{ selected: false }
);
}
selectedFeatureId = feature.id;
map.setFeatureState(
{ source: 'cities', id: selectedFeatureId },
{ selected: true }
);
});
Такой шаблон формирует базу для более сложных сценариев взаимодействия, включая множественный выбор, drag-selection и контекстные панели.