Simple select

Выбор объектов на карте в 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 (если задан в данных)
  • geometry
  • properties
  • layer

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


Базовая модель одиночного выбора

Простейшая логика выбора предполагает хранение одного активного объекта. При каждом клике предыдущий выбор сбрасывается, новый устанавливается как активный.

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. Без него невозможна стабильная идентификация выбранного элемента.


Подсветка выбранного объекта через filter

Один из наиболее прямолинейных способов визуального выделения — использование фильтра слоя. Слой дублируется: один отображает все объекты, второй — только выбранный.

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 для выделения

Более гибкий и производительный способ основан на 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 зависит от частоты событий и сложности слоёв. При высокой плотности данных возможны следующие узкие места:

  • большое количество объектов в queryRenderedFeatures
  • сложные выражения paint
  • частые обновления feature-state

Оптимизация достигается через:

  • ограничение layers в запросе
  • минимизацию количества активных состояний
  • использование простых выражений в стилях
  • отказ от постоянного пересчёта фильтров

Сравнение подходов выбора

Использование filter:

  • простая реализация
  • требует дублирования слоя
  • быстрое переключение

Использование feature-state:

  • отсутствие дублирования
  • масштабируемость
  • более сложная логика обновления

Использование queryRenderedFeatures:

  • основа всех сценариев
  • работает только с отображаемыми объектами
  • зависит от текущего зума и стиля

Структура минимального сценария выбора

Полный цикл простого выбора включает три этапа:

  1. Получение объекта через queryRenderedFeatures
  2. Сохранение идентификатора выбранного элемента
  3. Визуализация состояния через feature-state или filter
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];

    if (selectedFeatureId !== null) {
        map.setFeatureState(
            { source: 'cities', id: selectedFeatureId },
            { selected: false }
        );
    }

    selectedFeatureId = feature.id;

    map.setFeatureState(
        { source: 'cities', id: selectedFeatureId },
        { selected: true }
    );
});

Такой шаблон формирует базу для более сложных сценариев взаимодействия, включая множественный выбор, drag-selection и контекстные панели.