Выделение выбранных объектов

Выделение объектов на карте в 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-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 });
  }
});

Такой подход применяется для сценариев выбора нескольких объектов, например при построении фильтров или групповых операций.

Hover-выделение

Выделение при наведении реализуется через событие 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
      });
    }
  );
});

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

Синхронизация выделения с UI

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

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'
]

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