В 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)
);
}
При использовании 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;
});
}
Этот метод часто применяется для поиска объектов «рядом с точкой интереса».
Реальные сценарии почти всегда требуют многослойной фильтрации: сначала ограничение по видимой области, затем по атрибутам, затем по геометрии.
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);
Такой конвейер снижает вычислительную нагрузку за счёт раннего отсечения лишних объектов.
Векторные тайлы оптимизированы для рендеринга, а не для геопространственных запросов. Поэтому:
within-оператора для всех слоёвЭто приводит к необходимости комбинировать:
queryRenderedFeatures для визуального слояПри увеличении количества объектов основная нагрузка возникает не в 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 не выполняет геометрическую
фильтрацию напрямую, он позволяет хранить результат пространственных
вычислений и использовать его в стилях.
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.
В сложных приложениях геометрическая фильтрация обычно распределяется по уровням:
Такое разделение позволяет удерживать производительность даже при десятках тысяч объектов и сложных полигональных запросах.