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

Отображение результатов поиска в OpenLayers начинается с организации канала получения геокодированных данных. Чаще всего используются внешние сервисы — Nominatim (OpenStreetMap), Pelias, Photon или собственные API. Независимо от источника, результат приводится к единому виду: массив объектов с координатами и описанием.

Типичная структура ответа:

  • название объекта
  • координаты (lon, lat)
  • тип объекта (улица, город, организация)
  • дополнительная метаинформация

Ключевой момент — нормализация данных перед добавлением в карту.

async function search(query) {
  const response = await fetch(
    `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`
  );
  const data = await response.json();

  return data.map(item => ({
    name: item.display_name,
    lon: parseFloat(item.lon),
    lat: parseFloat(item.lat)
  }));
}

Преобразование координат в систему карты

OpenLayers работает в проекции Web Mercator (EPSG:3857), тогда как большинство геокодеров возвращают координаты в EPSG:4326. Перед добавлением результатов в слой необходимо выполнить преобразование.

import {fromLonLat} from 'ol/proj';

function toMapCoordinate(lon, lat) {
  return fromLonLat([lon, lat]);
}

Игнорирование преобразования приводит к смещению объектов и некорректному позиционированию на карте.


Создание слоя для отображения результатов

Результаты поиска обычно отображаются через VectorLayer с источником VectorSource. Каждый результат становится объектом Feature.

import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';

const searchSource = new VectorSource();

const searchLayer = new VectorLayer({
  source: searchSource
});

Добавление результата:

function addSearchResult(result) {
  const feature = new Feature({
    geometry: new Point(toMapCoordinate(result.lon, result.lat)),
    name: result.name
  });

  searchSource.addFeature(feature);
}

Визуальное оформление точек поиска

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

import Style from 'ol/style/Style';
import CircleStyle from 'ol/style/Circle';
import Fill from 'ol/style/Fill';
import Stroke from 'ol/style/Stroke';

const searchStyle = new Style({
  image: new CircleStyle({
    radius: 6,
    fill: new Fill({ color: 'rgba(255, 0, 0, 0.8)' }),
    stroke: new Stroke({ color: '#ffffff', width: 2 })
  })
});

Применение стиля:

const searchLayer = new VectorLayer({
  source: searchSource,
  style: searchStyle
});

Центрирование и масштабирование по результату

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

import View from 'ol/View';

function zoomToResult(map, result) {
  const coord = toMapCoordinate(result.lon, result.lat);

  map.getView().animate({
    center: coord,
    zoom: 14,
    duration: 800
  });
}

При работе с несколькими результатами используется расчет границ:

import {boundingExtent} from 'ol/extent';

function zoomToAll(map, results) {
  const extent = boundingExtent(
    results.map(r => toMapCoordinate(r.lon, r.lat))
  );

  map.getView().fit(extent, {
    padding: [50, 50, 50, 50],
    duration: 800
  });
}

Синхронизация с пользовательским вводом

Поиск обычно привязан к текстовому полю. Для уменьшения количества запросов применяется debounce.

function debounce(fn, delay) {
  let timer;

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

Применение:

const handleSearch = debounce(async (value) => {
  const results = await search(value);

  searchSource.clear();

  results.forEach(addSearchResult);
}, 400);

input.addEventListener('input', (e) => {
  handleSearch(e.target.value);
});

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

Помимо карты, результаты часто выводятся в виде списка. Каждый элемент списка связан с соответствующим Feature.

function renderResultsList(results) {
  const container = document.getElementById('results');
  container.innerHTML = '';

  results.forEach((r, index) => {
    const item = document.createElement('div');
    item.textContent = r.name;

    item.addEventListener('click', () => {
      zoomToResult(map, r);
      highlightFeature(index);
    });

    container.appendChild(item);
  });
}

Выделение выбранного результата

Для визуального акцента используется изменение стиля конкретного Feature или временный слой выделения.

function highlightFeature(feature) {
  feature.setStyle(
    new Style({
      image: new CircleStyle({
        radius: 10,
        fill: new Fill({ color: 'rgba(0, 120, 255, 0.9)' }),
        stroke: new Stroke({ color: '#fff', width: 2 })
      })
    })
  );
}

При необходимости предыдущие выделения сбрасываются установкой null стиля.


Работа с bounding box и ограничением области поиска

Для повышения релевантности запросов используется ограничение области поиска по текущему виду карты.

function getSearchBounds(map) {
  const extent = map.getView().calculateExtent(map.getSize());

  return extent;
}

Передача bounding box в API:

const extent = getSearchBounds(map);

const [minX, minY, maxX, maxY] = extent;

const url = `https://nominatim.openstreetmap.org/search?format=json&bounded=1&viewbox=${minX},${maxY},${maxX},${minY}&q=${query}`;

Очистка и обновление результатов

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

function clearResults() {
  searchSource.clear();
}

При динамическом поиске порядок операций фиксируется:

  1. очистка слоя
  2. запрос данных
  3. добавление новых features
  4. обновление списка
  5. при необходимости — изменение масштаба

Обработка пустых и ошибочных ответов

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

if (!results.length) {
  searchSource.clear();
  return;
}

Также учитываются сетевые ошибки:

try {
  const results = await search(query);
  updateMap(results);
} catch (e) {
  searchSource.clear();
}

Кластеризация результатов поиска

При большом количестве объектов используется кластеризация через Cluster источник.

import Cluster from 'ol/source/Cluster';

const clusterSource = new Cluster({
  distance: 40,
  source: searchSource
});

Это уменьшает нагрузку на визуализацию и улучшает читаемость карты при плотных данных.


Согласование списка и карты

Состояние интерфейса синхронизируется между списком и картой через общий источник данных. Любое изменение в массиве результатов приводит к:

  • пересозданию Feature
  • обновлению DOM списка
  • пересчету границ отображения
  • сбросу предыдущих выделений

Такая связка предотвращает рассинхронизацию и упрощает управление состоянием поискового слоя.