Configuring Store Location Filtering on a Map in 1C-Bitrix
A retail network with 50 locations across cities, different store formats, different operating hours. A user wants to find the nearest store with the right assortment that is open right now. The standard bitrix:map.yandex.view component simply displays all pins without filtering. Filtering is custom functionality.
Storing Store Data
Store locations are more conveniently stored in an infoblock rather than the standard b_catalog_store directory. An infoblock provides flexibility in fields, multilingual support, and a convenient editor in the admin panel.
Stores infoblock (e.g., STORES):
| Property | Code | Type |
|---|---|---|
| Address | ADDRESS |
String |
| City | CITY |
List |
| Format | FORMAT |
List (Hypermarket / Supermarket / Neighbourhood store) |
| Hours | SCHEDULE |
String |
| Latitude | LAT |
Number |
| Longitude | LON |
Number |
| Phone | PHONE |
String |
| Metro station | METRO |
String |
API Endpoint for the Map
The map frontend operates via AJAX. A PHP endpoint accepts filter parameters and returns locations:
// /local/ajax/stores.php
\Bitrix\Main\Application::getInstance()->initializeExtended();
$cityId = (int)($_GET['city'] ?? 0);
$format = trim($_GET['format'] ?? '');
$openNow = ($_GET['open_now'] ?? '') === '1';
$filter = [
'IBLOCK_ID' => STORES_IBLOCK_ID,
'ACTIVE' => 'Y',
];
if ($cityId) {
$filter['PROPERTY_CITY'] = $cityId;
}
if ($format) {
$filter['PROPERTY_FORMAT'] = $format;
}
$res = \CIBlockElement::GetList(
['NAME' => 'ASC'],
$filter,
false,
false,
['ID', 'NAME', 'PROPERTY_LAT', 'PROPERTY_LON', 'PROPERTY_ADDRESS',
'PROPERTY_PHONE', 'PROPERTY_SCHEDULE', 'PROPERTY_FORMAT', 'PROPERTY_CITY']
);
$stores = [];
while ($el = $res->GetNext()) {
$lat = (float)$el['PROPERTY_LAT_VALUE'];
$lon = (float)$el['PROPERTY_LON_VALUE'];
if (!$lat || !$lon) continue;
if ($openNow && !isOpenNow($el['PROPERTY_SCHEDULE_VALUE'])) {
continue;
}
$stores[] = [
'id' => $el['ID'],
'name' => $el['NAME'],
'address' => $el['PROPERTY_ADDRESS_VALUE'],
'phone' => $el['PROPERTY_PHONE_VALUE'],
'schedule' => $el['PROPERTY_SCHEDULE_VALUE'],
'format' => $el['PROPERTY_FORMAT_VALUE'],
'lat' => $lat,
'lon' => $lon,
];
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['stores' => $stores, 'count' => count($stores)]);
Determining "Open Now"
Operating hours are stored as a string such as "Mon–Fri: 9:00–21:00, Sat–Sun: 10:00–20:00". A function parses the string and checks the current time:
function isOpenNow(string $schedule): bool
{
$now = new DateTime('now', new DateTimeZone('Europe/Moscow'));
$dayNum = (int)$now->format('N'); // 1=Mon, 7=Sun
$timeStr = $now->format('H:i');
// Parse pattern "Mon-Fri: 9:00-21:00"
preg_match_all('/([А-Яа-я-]+):\s*(\d+:\d+)-(\d+:\d+)/u', $schedule, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
if (dayRangeCovers($m[1], $dayNum) && timeInRange($timeStr, $m[2], $m[3])) {
return true;
}
}
return false;
}
Frontend: Yandex Maps + Filter
// Map and filter initialisation
ymaps.ready(async function() {
const map = new ymaps.Map('store-map', { center: [55.76, 37.64], zoom: 10 });
const clusterer = new ymaps.Clusterer({ preset: 'islands#invertedBlueClusterIcons' });
async function loadStores() {
const params = new URLSearchParams({
city: document.getElementById('filter-city').value,
format: document.getElementById('filter-format').value,
open_now: document.getElementById('filter-open').checked ? '1' : '0',
});
const data = await fetch('/local/ajax/stores.php?' + params).then(r => r.json());
clusterer.removeAll();
map.geoObjects.remove(clusterer);
const placemarks = data.stores.map(store => {
const pm = new ymaps.Placemark(
[store.lat, store.lon],
{
balloonContentHeader: store.name,
balloonContentBody:
`<b>${store.address}</b><br>${store.phone}<br>${store.schedule}`,
hintContent: store.name,
},
{ preset: 'islands#blueDotIcon' }
);
return pm;
});
clusterer.add(placemarks);
map.geoObjects.add(clusterer);
document.getElementById('store-count').textContent = data.count;
}
// Reload on filter change
document.querySelectorAll('.store-filter').forEach(el => {
el.addEventListener('change', loadStores);
});
loadStores();
});
Geolocation "Near Me"
When the user clicks "Show nearest stores", the browser requests their coordinates and sorts locations by distance:
navigator.geolocation.getCurrentPosition(pos => {
const userLat = pos.coords.latitude;
const userLon = pos.coords.longitude;
// Sort by distance (Haversine approximation — sufficient for short distances)
stores.sort((a, b) => {
const da = Math.hypot(a.lat - userLat, a.lon - userLon);
const db = Math.hypot(b.lat - userLat, b.lon - userLon);
return da - db;
});
// Pan the map to the user's location
map.panTo([userLat, userLon], { duration: 500 });
});
Caching
The store list changes infrequently. Data is cached in the Bitrix file cache for 3600 seconds and invalidated when an infoblock element is edited, via the OnAfterIBlockElementUpdate event handler.
| Configuration | Timeline |
|---|---|
| Basic map with city/format filters | 3–4 days |
| + geolocation, "open now", clustering | +2–3 days |
| + assortment integration (filter by products available at a location) | +1 week |







