Поиск ближайших точек

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

В 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

На практике поиск ближайших объектов обычно реализуется через библиотеку 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() автоматически вычисляет расстояния и возвращает ближайший объект.

Преимущества:

  • высокая точность;
  • компактный код;
  • поддержка GeoJSON;
  • совместимость с MapLibre GL JS.

Отображение результата на карте

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

Добавление источника:

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);

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


Поиск ближайших N объектов

Во многих системах требуется не один объект, а несколько ближайших.

Пример сортировки:

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 вычислений расстояния
для каждого запроса

При интерактивном поиске это может вызывать заметные задержки.


Пространственная индексация

Для ускорения поиска используются пространственные индексы:

  • R-Tree;
  • RBush;
  • KD-Tree;
  • QuadTree.

Популярным решением для GeoJSON является RBush.

Установка:

npm install rbush

Создание индекса:

const tree = new RBush();

Заполнение:

tree.insert({
  minX: 37.60,
  minY: 55.75,
  maxX: 37.60,
  maxY: 55.75,
  feature
});

После построения индекса поиск выполняется значительно быстрее, поскольку проверяется лишь часть объектов.


Использование queryRenderedFeatures

Если интересуют только объекты, видимые на экране, можно использовать встроенные возможности 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
  );
});

В результате поиск ограничивается текущим видимым фрагментом карты, что уменьшает объём вычислений и повышает скорость работы интерфейса.

Типичный сценарий реализации

Полный алгоритм поиска ближайшей точки обычно состоит из следующих этапов:

  1. Загрузка GeoJSON-данных.
  2. Получение координат пользователя или точки выбора.
  3. Поиск ближайшего объекта через Turf.js либо собственный алгоритм.
  4. Вычисление расстояния.
  5. Обновление GeoJSON-источника.
  6. Выделение найденного объекта на карте.
  7. Отображение информации во всплывающем окне или боковой панели.
  8. Повторное выполнение поиска при изменении положения пользователя или взаимодействии с картой.

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