В интерактивных картах на основе Mapbox GL JS одной из ключевых задач является определение ближайшего географического объекта к заданной точке. Такая операция используется при кликах по карте, построении маршрутов, выборе POI, реализации инструментов анализа и динамической подсветки данных.
В контексте векторного рендеринга карта не хранит «готовые расстояния» между объектами. Все вычисления выполняются либо на стороне клиентского кода, либо с использованием геопространственных библиотек. Под «NearestPoint» обычно понимается алгоритм поиска ближайшей точки или объекта в наборе геометрий относительно заданной координаты.
Любой поиск ближайшего объекта сводится к вычислению минимального расстояния между точкой запроса и множеством геометрий:
Расстояние может определяться:
В веб-картах чаще всего используются два подхода:
В Mapbox GL JS данные слоя не доступны напрямую как массив геометрий. Вместо этого используется выборка через рендер-слой:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
console.log(features);
});
Метод queryRenderedFeatures возвращает объекты, которые
уже прошли этап рендеринга и видимы в текущем масштабе.
Каждый feature содержит:
Однако этот метод не возвращает «ближайший» объект автоматически. Он лишь ограничивает набор кандидатов.
Базовый алгоритм поиска ближайшей точки можно реализовать через линейный проход по всем объектам:
function getDistance(a, b) {
const dx = a[0] - b[0];
const dy = a[1] - b[1];
return Math.sqrt(dx * dx + dy * dy);
}
function findNearestPoint(clickCoord, features) {
let nearest = null;
let minDistance = Infinity;
for (const feature of features) {
const coords = feature.geometry.coordinates;
const distance = getDistance(clickCoord, coords);
if (distance < minDistance) {
minDistance = distance;
nearest = feature;
}
}
return nearest;
}
Такой подход применим только для Point-геометрий. Для линий и полигонов требуется вычисление расстояния до сегмента.
Для LineString и Polygon необходимо вычислять минимальное расстояние до отрезков.
Упрощённая логика:
Пример вычисления расстояния от точки до отрезка:
function distanceToSegment(p, a, b) {
const x = p[0], y = p[1];
const x1 = a[0], y1 = a[1];
const x2 = b[0], y2 = b[1];
const dx = x2 - x1;
const dy = y2 - y1;
const t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
const clampedT = Math.max(0, Math.min(1, t));
const projX = x1 + clampedT * dx;
const projY = y1 + clampedT * dy;
const distX = x - projX;
const distY = y - projY;
return Math.sqrt(distX * distX + distY * distY);
}
Для полигона алгоритм аналогичен, но учитываются все ребра внешнего и внутренних контуров.
В реальных приложениях ручные вычисления обычно заменяются специализированными библиотеками. Наиболее распространённый инструмент — Turf.
Функция nearestPoint позволяет находить ближайший объект
в наборе:
import nearestPoint from '@turf/nearest-point';
import { featureCollection, point } from '@turf/helpers';
const queryPoint = point([87.134, 53.756]);
const points = featureCollection([
point([87.120, 53.760]),
point([87.200, 53.700]),
point([87.140, 53.750])
]);
const nearest = nearestPoint(queryPoint, points);
Внутри используется геодезическое расстояние, что делает результат корректным для больших расстояний.
Типичный сценарий — обработка клика по карте и выбор ближайшего объекта слоя:
map.on('click', (e) => {
const clicked = [e.lngLat.lng, e.lngLat.lat];
const features = map.queryRenderedFeatures(e.point, {
layers: ['places']
});
let nearest = null;
let minDistance = Infinity;
for (const f of features) {
const coords = f.geometry.coordinates;
const dx = coords[0] - clicked[0];
const dy = coords[1] - clicked[1];
const d = dx * dx + dy * dy;
if (d < minDistance) {
minDistance = d;
nearest = f;
}
}
if (nearest) {
map.setFeatureState(
{ source: 'places-source', id: nearest.id },
{ selected: true }
);
}
});
Здесь используется квадрат расстояния вместо корня для оптимизации вычислений.
При большом количестве объектов линейный перебор становится неэффективным. Основные подходы оптимизации:
Эти структуры позволяют уменьшить количество проверяемых объектов с O(n) до O(log n).
В Mapbox GL JS часто применяется кластеризация через GeoJSON source:
map.addSource('points', {
type: 'geojson',
data: data,
cluster: true,
clusterRadius: 50
});
Кластеры уменьшают количество объектов, среди которых выполняется поиск.
При взаимодействии с пользователем важна разница между системами координат:
e.point — пикселиe.lngLat — географические координатыЭкранное расстояние используется для UI-подсветки, так как визуально соответствует восприятию пользователя.
Пример:
const p1 = map.project([lng, lat]);
const p2 = e.point;
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
const screenDistance = Math.sqrt(dx * dx + dy * dy);
В задачах навигации часто требуется определить ближайшую точку на маршруте:
Алгоритм включает:
Mapbox GL JS позволяет ограничивать область поиска:
map.queryRenderedFeatures([
[x - 10, y - 10],
[x + 10, y + 10]
], {
layers: ['roads']
});
Это снижает количество проверяемых объектов, ограничивая поиск радиусом в пикселях.
При работе с координатами важно учитывать:
Поэтому:
В реальных системах используется комбинация:
queryRenderedFeatures — фильтрация по видимой
областиТакая цепочка позволяет балансировать между производительностью и точностью без избыточных вычислений.