Отображение результатов поиска в OpenLayers начинается с организации канала получения геокодированных данных. Чаще всего используются внешние сервисы — Nominatim (OpenStreetMap), Pelias, Photon или собственные API. Независимо от источника, результат приводится к единому виду: массив объектов с координатами и описанием.
Типичная структура ответа:
Ключевой момент — нормализация данных перед добавлением в карту.
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 стиля.
Для повышения релевантности запросов используется ограничение области поиска по текущему виду карты.
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();
}
При динамическом поиске порядок операций фиксируется:
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Такая связка предотвращает рассинхронизацию и упрощает управление состоянием поискового слоя.