Direct select

В Mapbox GL JS под прямым выбором (direct select) понимается взаимодействие с конкретными географическими объектами, отрисованными на карте, без промежуточных списков, поисковых запросов или серверных фильтров. Основой такого подхода становится работа с уже загруженными в стиль слоями и геометриями, где пользовательское действие (клик, наведение, касание) преобразуется в запрос к отрисованным данным.

Ключевая идея заключается в том, что карта выступает как интерактивный рендерер WebGL-сцены, а выбор объекта реализуется через попадание курсора в область отрисованного фрагмента слоя.

Поиск объектов под курсором через queryRenderedFeatures

Основной механизм прямого выбора в Mapbox GL JS реализуется через метод:

map.queryRenderedFeatures(point, options)

Этот метод возвращает массив объектов, которые в данный момент отрисованы в области экрана, соответствующей координате курсора.

Базовый пример выбора

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point);

  if (!features.length) return;

  console.log(features[0]);
});

Фильтрация по слоям

При сложной карте с множеством слоёв критически важно ограничивать область поиска:

map.queryRenderedFeatures(e.point, {
  layers: ['cities-layer']
});

Это позволяет исключить фоновые или вспомогательные слои и работать только с интерактивными объектами.

Ограничение по геометрии

Вместо точки можно использовать прямоугольную область:

map.queryRenderedFeatures([
  [e.point.x - 5, e.point.y - 5],
  [e.point.x + 5, e.point.y + 5]
]);

Такой подход используется для расширения зоны захвата при неточных взаимодействиях.

Определение приоритета объектов

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

const feature = features[0];

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

Состояние объектов через feature-state

Одним из ключевых инструментов реализации прямого выбора является система состояний объектов:

map.setFeatureState(
  {
    source: 'places',
    id: 123
  },
  {
    selected: true
  }
);

Использование идентификаторов

Для работы feature-state источник данных должен содержать уникальные id:

{
  "type": "Feature",
  "id": 42,
  "geometry": { ... },
  "properties": { ... }
}

Сброс состояния

map.setFeatureState(
  { source: 'places', id: 42 },
  { selected: false }
);

Визуализация выбранного объекта

Отображение состояния обычно реализуется через paint-свойства слоёв и выражения.

Подсветка через case expression

map.setPaintProperty('places-layer', 'circle-color', [
  'case',
  ['boolean', ['feature-state', 'selected'], false],
  '#ff0000',
  '#3388ff'
]);

Такой подход позволяет отделить логику выбора от рендеринга.

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

map.setPaintProperty('places-layer', 'circle-radius', [
  'case',
  ['boolean', ['feature-state', 'selected'], false],
  10,
  6
]);

Hover-взаимодействие как часть direct select

Помимо клика часто используется наведение курсора:

map.on('mousemove', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['places-layer']
  });

  map.getCanvas().style.cursor = features.length ? 'pointer' : '';
});

Ограничение частоты обновления

При интенсивных данных используется throttle:

let lastFeatureId = null;

map.on('mousemove', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['places-layer']
  });

  const feature = features[0];

  if (!feature || feature.id === lastFeatureId) return;

  lastFeatureId = feature.id;
});

Управление единственным выбранным объектом

В типичном сценарии direct select предполагает, что одновременно выбран только один объект.

let selectedId = null;

function selectFeature(feature) {
  if (selectedId !== null) {
    map.setFeatureState(
      { source: 'places', id: selectedId },
      { selected: false }
    );
  }

  selectedId = feature.id;

  map.setFeatureState(
    { source: 'places', id: selectedId },
    { selected: true }
  );
}

Работа с источниками GeoJSON

Direct select часто используется совместно с GeoJSON источниками:

map.addSource('places', {
  type: 'geojson',
  data: 'places.geojson'
});

И слоем:

map.addLayer({
  id: 'places-layer',
  type: 'circle',
  source: 'places',
  paint: {
    'circle-radius': 6,
    'circle-color': '#3388ff'
  }
});

Обработка перекрывающихся объектов

При плотной визуализации возникает проблема перекрытия. В этом случае применяются стратегии:

1. Использование bbox-запроса с сортировкой

const features = map.queryRenderedFeatures(e.point, {
  layers: ['places-layer']
}).sort((a, b) => b.properties.priority - a.properties.priority);

2. Ограничение по минимальному масштабу

if (map.getZoom() < 10) return;

Интерактивность с полигонами и линиями

Direct select работает не только с точками, но и с любыми типами геометрий.

Полигональные объекты

map.queryRenderedFeatures(e.point, {
  layers: ['polygons-layer']
});

При этом попадание определяется по пиксельной триангуляции WebGL, а не по геометрическим вычислениям в JS.

Линии

Для линий часто используется расширение hitbox через line-width:

map.addLayer({
  id: 'routes',
  type: 'line',
  source: 'routes',
  paint: {
    'line-width': 8,
    'line-color': '#00a'
  }
});

Производственные ограничения и оптимизация

При частых вызовах queryRenderedFeatures важно учитывать стоимость операций.

Ограничение частоты запросов

let scheduled = false;

map.on('mousemove', (e) => {
  if (scheduled) return;

  scheduled = true;

  requestAnimationFrame(() => {
    map.queryRenderedFeatures(e.point);
    scheduled = false;
  });
});

Минимизация количества слоёв

Чем меньше интерактивных слоёв, тем быстрее работает выбор.

Комбинация direct select с состояниями UI

Часто direct select связывается с внешним UI-состоянием:

map.on('click', (e) => {
  const feature = map.queryRenderedFeatures(e.point, {
    layers: ['places-layer']
  })[0];

  if (!feature) return;

  updateSidebar(feature.properties);
});

Разделение логики выбора и логики рендера

Архитектурно важно отделять:

  • определение выбранного объекта (queryRenderedFeatures)
  • хранение состояния (feature-state или store)
  • визуализацию (paint expressions)

Такая структура предотвращает дублирование логики и снижает связность компонентов.

Сложные сценарии: комбинированный direct select

В многослойных картах выбор может происходить по приоритету типов объектов:

const layersPriority = [
  'pois-layer',
  'roads-layer',
  'buildings-layer'
];

const features = map.queryRenderedFeatures(e.point, {
  layers: layersPriority
});

Первый найденный объект становится активным независимо от геометрического типа.

Интеграция с редактированием геометрий

Direct select часто является основой для редакторов геоданных, где после выбора активируется режим трансформации:

  • перемещение точки
  • изменение вершин полигона
  • перетаскивание линии

Выбранный объект фиксируется через feature-state, а дальнейшие операции работают с его координатами через source update.