Setting up point filtering on the 1C-Bitrix store map

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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