Одной из наиболее востребованных задач в геоинформационных приложениях является определение объектов, находящихся ближе всего к заданной позиции. В контексте интерактивных карт это может использоваться для поиска ближайшего магазина, банкомата, парковки, остановки общественного транспорта, склада или любого другого пространственного объекта.
В MapLibre GL JS отображение данных на карте тесно связано с источниками данных GeoJSON, что позволяет эффективно реализовывать поиск ближайших объектов непосредственно на стороне клиента.
Предположим, имеется набор точек:
Необходимо определить объект, который расположен ближе всего к координатам пользователя или к точке, выбранной на карте.
Исходные данные обычно представлены в формате GeoJSON:
const stores = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {
name: "Магазин №1"
},
geometry: {
type: "Point",
coordinates: [37.6176, 55.7558]
}
},
{
type: "Feature",
properties: {
name: "Магазин №2"
},
geometry: {
type: "Point",
coordinates: [37.6050, 55.7610]
}
}
]
};
Каждая точка содержит координаты и дополнительные атрибуты.
Наиболее очевидный способ — вычислить расстояние до каждой точки и выбрать минимальное значение.
Координаты пользователя:
const userLocation = [37.6100, 55.7580];
Функция поиска:
function findNearestPoint(target, features) {
let nearest = null;
let minDistance = Infinity;
for (const feature of features) {
const coords = feature.geometry.coordinates;
const distance = Math.sqrt(
Math.pow(coords[0] - target[0], 2) +
Math.pow(coords[1] - target[1], 2)
);
if (distance < minDistance) {
minDistance = distance;
nearest = feature;
}
}
return nearest;
}
Использование:
const nearestStore = findNearestPoint(
userLocation,
stores.features
);
console.log(nearestStore.properties.name);
Такой подход подходит для небольших наборов данных, однако имеет серьёзный недостаток: вычисления выполняются в плоской системе координат, тогда как координаты на карте представлены в географической системе WGS84.
Координаты широты и долготы являются угловыми величинами.
Разница между долготами:
37.61 - 37.60 = 0.01
не соответствует фиксированному расстоянию на разных широтах.
Поэтому для корректного поиска необходимо вычислять реальное расстояние на поверхности Земли.
Для определения расстояния между двумя точками на сфере широко используется формула гаверсинусов.
Функция вычисления расстояния:
function haversineDistance(coord1, coord2) {
const R = 6371000;
const lon1 = coord1[0] * Math.PI / 180;
const lat1 = coord1[1] * Math.PI / 180;
const lon2 = coord2[0] * Math.PI / 180;
const lat2 = coord2[1] * Math.PI / 180;
const dLon = lon2 - lon1;
const dLat = lat2 - lat1;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1) *
Math.cos(lat2) *
Math.sin(dLon / 2) ** 2;
const c =
2 * Math.atan2(
Math.sqrt(a),
Math.sqrt(1 - a)
);
return R * c;
}
Поиск ближайшего объекта:
function findNearestPoint(target, features) {
let nearest = null;
let minDistance = Infinity;
for (const feature of features) {
const distance = haversineDistance(
target,
feature.geometry.coordinates
);
if (distance < minDistance) {
minDistance = distance;
nearest = feature;
}
}
return nearest;
}
Теперь результаты будут корректными независимо от региона карты.
На практике поиск ближайших объектов обычно реализуется через библиотеку Turf.js.
Подключение:
<script src="https://unpkg.com/@turf/turf@latest/turf.min.js"></script>
Поиск ближайшей точки:
const currentPoint = turf.point(userLocation);
const nearest = turf.nearestPoint(
currentPoint,
stores
);
console.log(nearest.properties.name);
Функция nearestPoint() автоматически вычисляет
расстояния и возвращает ближайший объект.
Преимущества:
После нахождения ближайшей точки часто требуется визуально выделить её.
Добавление источника:
map.addSource("nearest-store", {
type: "geojson",
data: nearest
});
Добавление слоя:
map.addLayer({
id: "nearest-store-layer",
type: "circle",
source: "nearest-store",
paint: {
"circle-radius": 12,
"circle-color": "#ff0000"
}
});
Ближайший объект будет отображён красным кругом.
Часто ближайший объект ищется относительно точки, выбранной пользователем.
Обработчик события:
map.on("click", (e) => {
const clickedPoint = turf.point([
e.lngLat.lng,
e.lngLat.lat
]);
const nearest = turf.nearestPoint(
clickedPoint,
stores
);
console.log(
nearest.properties.name
);
});
После каждого щелчка будет определяться новый ближайший объект.
Для наглядности можно отображать место клика.
Создание маркера:
map.on("click", (e) => {
new maplibregl.Marker()
.setLngLat(e.lngLat)
.addTo(map);
});
Более удобным вариантом является использование отдельного GeoJSON-источника с последующим обновлением данных.
При повторных запросах слой не следует создавать заново.
Источник создаётся один раз:
map.addSource("nearest-store", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
Затем данные обновляются:
map.getSource("nearest-store")
.setData(nearest);
Такой подход обеспечивает более высокую производительность.
После нахождения ближайшей точки часто требуется показать расстояние.
Вычисление:
const distance = turf.distance(
clickedPoint,
nearest,
{
units: "kilometers"
}
);
console.log(distance);
Перевод в метры:
const meters =
Math.round(distance * 1000);
console.log(
`${meters} метров`
);
Информация о ближайшем объекте может отображаться через Popup.
new maplibregl.Popup()
.setLngLat(
nearest.geometry.coordinates
)
.setHTML(`
<h3>${nearest.properties.name}</h3>
<p>${meters} м</p>
`)
.addTo(map);
Пользователь получает название объекта и расстояние до него.
Во многих системах требуется не один объект, а несколько ближайших.
Пример сортировки:
const sorted = stores.features
.map(feature => ({
feature,
distance: haversineDistance(
userLocation,
feature.geometry.coordinates
)
}))
.sort(
(a, b) =>
a.distance - b.distance
);
Получение пяти ближайших объектов:
const nearestFive =
sorted.slice(0, 5);
Каждый элемент массива содержит объект и расстояние до него.
Создание структуры данных:
const nearestList =
sorted.slice(0, 5)
.map(item => ({
name: item.feature.properties.name,
distance:
Math.round(item.distance)
}));
Результат:
[
{
name: "Магазин №2",
distance: 145
},
{
name: "Магазин №5",
distance: 287
}
]
Такой список может использоваться для боковой панели приложения.
Иногда требуется найти не самый близкий объект, а все объекты внутри определённого расстояния.
Использование Turf.js:
const nearby = stores.features.filter(
feature => {
const distance = turf.distance(
clickedPoint,
feature,
{
units: "kilometers"
}
);
return distance <= 2;
}
);
В примере выбираются все точки в радиусе двух километров.
Получение координат через браузерный API:
navigator.geolocation.getCurrentPosition(
(position) => {
const coords = [
position.coords.longitude,
position.coords.latitude
];
const nearest =
turf.nearestPoint(
turf.point(coords),
stores
);
console.log(
nearest.properties.name
);
}
);
Полученные координаты могут использоваться для автоматического поиска ближайших объектов сразу после загрузки карты.
При наличии десятков тысяч точек полный перебор становится дорогой операцией.
Проблемы:
Например:
10 000 точек
=
10 000 вычислений расстояния
для каждого запроса
При интерактивном поиске это может вызывать заметные задержки.
Для ускорения поиска используются пространственные индексы:
Популярным решением для GeoJSON является RBush.
Установка:
npm install rbush
Создание индекса:
const tree = new RBush();
Заполнение:
tree.insert({
minX: 37.60,
minY: 55.75,
maxX: 37.60,
maxY: 55.75,
feature
});
После построения индекса поиск выполняется значительно быстрее, поскольку проверяется лишь часть объектов.
Если интересуют только объекты, видимые на экране, можно использовать встроенные возможности MapLibre GL JS.
Получение объектов:
const features =
map.queryRenderedFeatures();
Получение объектов конкретного слоя:
const features =
map.queryRenderedFeatures({
layers: ["stores"]
});
Далее поиск ближайшей точки выполняется только среди отображаемых объектов.
Это особенно полезно при работе с большими наборами данных и кластеризацией.
Пример объединения возможностей MapLibre GL JS и Turf.js:
map.on("click", (e) => {
const visibleFeatures =
map.queryRenderedFeatures({
layers: ["stores"]
});
const collection = {
type: "FeatureCollection",
features: visibleFeatures
};
const nearest =
turf.nearestPoint(
turf.point([
e.lngLat.lng,
e.lngLat.lat
]),
collection
);
console.log(
nearest.properties.name
);
});
В результате поиск ограничивается текущим видимым фрагментом карты, что уменьшает объём вычислений и повышает скорость работы интерфейса.
Полный алгоритм поиска ближайшей точки обычно состоит из следующих этапов:
Подобная схема используется в навигационных системах, сервисах доставки, логистических платформах, туристических приложениях и корпоративных геоинформационных системах, где требуется быстро определять наиболее близкие пространственные объекты относительно произвольной точки на карте.