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

В MapLibre GL JS работа с геометрической фильтрацией чаще всего строится вокруг двух уровней: фильтрации на стороне источника данных и постобработки уже отрисованных или доступных на карте объектов. Векторные тайлы не предполагают полноценных пространственных запросов, поэтому основная логика геометрической фильтрации реализуется на клиенте.

Базовый инструмент для извлечения объектов в пределах видимой области — queryRenderedFeatures. Он возвращает набор фич, которые уже попали в текущий viewport.

const features = map.queryRenderedFeatures();

Для ограничения области используется bounding box:

const bbox = map.getBounds();

const features = map.queryRenderedFeatures(
  [
    [bbox.getWest(), bbox.getSouth()],
    [bbox.getEast(), bbox.getNorth()]
  ]
);

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


Фильтрация по произвольному полигону

Прямоугольная область часто недостаточна, особенно при работе с административными границами или пользовательскими контурами. В таких случаях используется проверка попадания точки в полигон.

Чаще всего применяется библиотека Turf.js:

import booleanPointInPolygon from '@turf/boolean-point-in-polygon';

function filterByPolygon(features, polygon) {
  return features.filter(feature => {
    const geometry = feature.geometry;

    if (geometry.type !== 'Point') return false;

    return booleanPointInPolygon(geometry.coordinates, polygon);
  });
}

Если данные представлены линиями или полигонами, требуется более сложная проверка пересечения:

import booleanIntersects from '@turf/boolean-intersects';

function filterByIntersection(features, polygon) {
  return features.filter(feature =>
    booleanIntersects(feature, polygon)
  );
}

Геометрическая фильтрация через GeoJSON-источники

При использовании GeoJSONSource фильтрация может выполняться на уровне источника через expression filter. Однако такие фильтры не поддерживают полноценную пространственную логику.

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

map.addLayer({
  id: 'points-layer',
  type: 'circle',
  source: 'points',
  paint: {
    'circle-radius': 6
  },
  filter: ['==', ['get', 'type'], 'restaurant']
});

Фильтрация здесь ограничивается атрибутами, а не геометрией. Поэтому пространственные операции всё равно выполняются вне expression-системы.


Ограничение области через экранные координаты

Часто требуется фильтрация не по географическим координатам, а по пиксельной области интерфейса.

const canvas = map.getCanvas();
const rect = canvas.getBoundingClientRect();

const bbox = [
  [0, 0],
  [rect.width, rect.height]
];

const features = map.queryRenderedFeatures(bbox);

Такой подход полезен при реализации выделения объектов рамкой (box sel ect).


Радиусная фильтрация (по расстоянию)

Для фильтрации по окружности используется вычисление расстояния между точками. MapLibre GL JS не предоставляет встроенного spatial radius filter, поэтому используется вычисление через Turf.js:

import distance fr om '@turf/distance';
import { point } from '@turf/helpers';

function filterByRadius(features, center, radiusKm) {
  const centerPoint = point(center);

  return features.filter(feature => {
    if (feature.geometry.type !== 'Point') return false;

    const featurePoint = point(feature.geometry.coordinates);

    const d = distance(centerPoint, featurePoint, { units: 'kilometers' });

    return d <= radiusKm;
  });
}

Этот метод часто применяется для поиска объектов «рядом с точкой интереса».


Комбинирование фильтров: bbox + атрибуты + геометрия

Реальные сценарии почти всегда требуют многослойной фильтрации: сначала ограничение по видимой области, затем по атрибутам, затем по геометрии.

const bbox = map.getBounds();

let features = map.queryRenderedFeatures([
  [bbox.getWest(), bbox.getSouth()],
  [bbox.getEast(), bbox.getNorth()]
]);

features = features.filter(f =>
  f.properties.category === 'cafe'
);

features = filterByPolygon(features, userPolygon);

Такой конвейер снижает вычислительную нагрузку за счёт раннего отсечения лишних объектов.


Работа с векторными тайлами и ограничения пространственных запросов

Векторные тайлы оптимизированы для рендеринга, а не для геопространственных запросов. Поэтому:

  • нельзя выполнить серверный spatial query
  • нет прямого within-оператора для всех слоёв
  • фильтрация всегда ограничена набором загруженных тайлов

Это приводит к необходимости комбинировать:

  • queryRenderedFeatures для визуального слоя
  • GeoJSON для аналитической логики
  • Turf.js или аналогичные библиотеки для геометрии

Оптимизация геометрической фильтрации

При увеличении количества объектов основная нагрузка возникает не в MapLibre, а в JavaScript-обработке.

Практические приёмы оптимизации:

1. Ограничение частоты пересчёта

map.on('moveend', () => {
  updateFilteredFeatures();
});

2. Кэширование результатов по зуму

const cache = new Map();

function getCached(z, compute) {
  if (cache.has(z)) return cache.get(z);

  const result = compute();
  cache.set(z, result);

  return result;
}

3. Предфильтрация на уровне данных

При загрузке GeoJSON можно заранее разбивать данные по категориям или пространственным индексам (например, R-tree).


Использование feature-state как вспомогательного механизма

Хотя feature-state не выполняет геометрическую фильтрацию напрямую, он позволяет хранить результат пространственных вычислений и использовать его в стилях.

map.setFeatureState(
  { source: 'points', id: featureId },
  { active: true }
);

Далее слой может визуально отражать результат фильтрации:

paint: {
  'circle-opacity': [
    'case',
    ['boolean', ['feature-state', 'active'], false],
    1,
    0.2
  ]
}

Это снижает необходимость постоянного пересчёта фильтров на уровне выражений.


Фильтрация по пересечению с видимой камерой

Иногда требуется не просто bbox, а точное пересечение с текущей камерой, включая вращение и наклон.

const canvas = map.getCanvas();
const width = canvas.width;
const height = canvas.height;

const corners = [
  map.unproject([0, 0]).toArray(),
  map.unproject([width, 0]).toArray(),
  map.unproject([width, height]).toArray(),
  map.unproject([0, height]).toArray()
];

Далее полученный контур можно использовать как полигон для Turf.js.


Гибридная архитектура пространственной фильтрации

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

  • MapLibre GL JS — рендеринг и первичное bbox-ограничение
  • GeoJSON + Turf.js — точная геометрия и аналитика
  • Сервер / индекс — предварительная агрегация и тайлинг
  • UI слой — управление состоянием и кешированием

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