PageSourceSearch

https://vnuivnu.ru/wa-files/js/vv-map.js?v=2

js vnuivnu.ru collected 2026-10-08 00:55:25 UTC 8,489 bytes, 177 lines download raw bytes

1/**
2 * Карты сайта без рекламы: зоны доставки и метка офиса на своём ключе JS API Яндекса.
3 *
4 * Зачем. Раньше карты вставлялись виджетом «Конструктора карт»
5 * (`api-maps.yandex.ru/services/constructor/1.0/js/?um=…`), и осенью 2026 Яндекс начал
6 * показывать в нём рекламу. Отключить её параметром нельзя, API-ключ на неё не влияет —
7 * он лишь добавляет поиск, маршруты и панорамы. В JS API рекламы нет, поэтому карта
8 * рисуется здесь самостоятельно.
9 *
10 * Зоны берутся из тех же файлов, по которым считается доставка
11 * (`sc-modules/yamaps-calculate/delimaps_<город>.geojson`), — карта и расчёт больше
12 * не могут разойтись: поправили зону в geojson, изменилась и картинка.
13 *
14 * Как вставлять на страницу (годится и в шаблоне, и в тексте страницы из админки):
15 *
16 *   <div class="vv-map" data-zones="spb" style="height:450px"></div>
17 *   <div class="vv-map" data-point="59.888848,30.482479" data-label="Внук и Внучка офис"></div>
18 *
19 * Города зон: spb, msk, nsk, rnd. Карта грузится лениво — API Яндекса подтягивается,
20 * только когда карта подъезжает к экрану, и только если карта на странице есть.
21 */
22(function () {
23    'use strict';
24
25    // тот же ключ, что у расчёта доставки: sc-modules/yamaps-calculate/yandex_api_keys.php
26    var API_KEY = 'cfa00a0f-9b09-4dda-a177-3899d03ad061';
27    var ZONES_URL = '/sc-modules/yamaps-calculate/delimaps_{city}.geojson';
28
29    // Насколько приблизить карту против рассчитанной по зонам: у Петербурга и Москвы
30    // зоны занимают меньшую часть кадра, поэтому на шаг крупнее. Можно задать и на месте
31    // атрибутом data-zoom-plus.
32    var ZOOM_PLUS = { spb: 1, msk: 1 };
33
34    var apiPromise = null;
35    var zonesCache = {};
36
37    /** Подключает JS API один раз на страницу */
38    function loadApi() {
39        if (!apiPromise) {
40            apiPromise = new Promise(function (resolve, reject) {
41                var script = document.createElement('script');
42                script.src = 'https://api-maps.yandex.ru/2.1/?apikey=' + API_KEY + '&lang=ru_RU';
43                script.onload = function () { ymaps.ready(resolve); };
44                script.onerror = reject;
45                document.head.appendChild(script);
46            });
47        }
48        return apiPromise;
49    }
50
51    /** Зоны города; один запрос на город, даже если карт на странице несколько */
52    function loadZones(city) {
53        if (!zonesCache[city]) {
54            zonesCache[city] = fetch(ZONES_URL.replace('{city}', city)).then(function (r) {
55                if (!r.ok) {
56                    throw new Error('зоны ' + city + ': ответ ' + r.status);
57                }
58                return r.json();
59            });
60        }
61        return zonesCache[city];
62    }
63
64    /** Общие настройки карты: без лишних кнопок и без перехвата прокрутки страницы */
65    function createMap(el, center, zoom) {
66        var map = new ymaps.Map(el, {
67            center: center,
68            zoom: zoom,
69            controls: ['zoomControl', 'fullscreenControl']
70        }, {
71            suppressMapOpenBlock: true      // без плашки «Открыть в Яндекс Картах»
72        });
73        map.behaviors.disable('scrollZoom');
74        if (window.matchMedia && window.matchMedia('(max-width: 767px)').matches) {
75            map.behaviors.disable('drag');  // на телефоне карта не должна ловить палец при прокрутке
76        }
77        return map;
78    }
79
80    /** Полигоны зон доставки: цвета и подписи берутся из самого geojson */
81    function drawZones(el, city) {
82        return loadZones(city).then(function (data) {
83            var map = createMap(el, [59.93, 30.33], 9);
84            var zones = new ymaps.GeoObjectCollection();
85
86            data.features.forEach(function (zone) {
87                // в geojson координаты идут [долгота, широта], карте нужен обратный порядок
88                var ring = zone.geometry.coordinates[0].map(function (c) { return [c[1], c[0]]; });
89                var p = zone.properties || {};
90                zones.add(new ymaps.Polygon([ring], {}, {
91                    fillColor: p.fill || '#1bad03',
92                    fillOpacity: typeof p['fill-opacity'] === 'number' ? p['fill-opacity'] : 0.3,
93                    strokeColor: p.stroke || '#1bad03',
94                    strokeWidth: parseInt(p['stroke-width'], 10) || 2,
95                    strokeOpacity: typeof p['stroke-opacity'] === 'number' ? p['stroke-opacity'] : 0.9
96                }));
97            });
98
99            map.geoObjects.add(zones);
100
101            var plus = el.hasAttribute('data-zoom-plus')
102                ? Number(el.getAttribute('data-zoom-plus'))
103                : (ZOOM_PLUS[city] || 0);
104
105            return map.setBounds(zones.getBounds(), { checkZoomRange: true, zoomMargin: 10 })
106                .then(function () {
107                    if (plus) {
108                        map.setZoom(map.getZoom() + plus, { checkZoomRange: true });
109                    }
110                });
111        });
112    }
113
114    /** Одна метка с подписью — для карты «где мы находимся» */
115    function drawPoint(el, coords, label) {
116        var map = createMap(el, coords, Number(el.getAttribute('data-zoom')) || 13);
117        map.geoObjects.add(new ymaps.Placemark(coords, {
118            iconCaption: label || '',
119            hintContent: label || ''
120        }, {
121            preset: 'islands#orangeFoodIcon'
122        }));
123    }
124
125    /** Рисует конкретную карту; ошибки не должны ронять остальную страницу */
126    function render(el) {
127        if (el.dataset.vvMapReady) {
128            return;
129        }
130        el.dataset.vvMapReady = '1';
131
132        loadApi().then(function () {
133            var zones = el.getAttribute('data-zones');
134            if (zones) {
135                return drawZones(el, zones);
136            }
137            var point = (el.getAttribute('data-point') || '').split(',').map(Number);
138            if (point.length === 2 && point.every(function (n) { return !isNaN(n); })) {
139                drawPoint(el, point, el.getAttribute('data-label'));
140            }
141        }).catch(function (e) {
142            el.dataset.vvMapReady = '';
143            if (window.console) {
144                console.warn('карта не построена:', e);
145            }
146        });
147    }
148
149    /** Ищет карты на странице и рисует их при приближении к экрану */
150    function init() {
151        var maps = document.querySelectorAll('.vv-map:not([data-vv-map-ready])');
152        if (!maps.length) {
153            return;
154        }
155        if (!window.IntersectionObserver) {
156            Array.prototype.forEach.call(maps, render);
157            return;
158        }
159        var watcher = new IntersectionObserver(function (entries) {
160            entries.forEach(function (entry) {
161                if (entry.isIntersecting) {
162                    watcher.unobserve(entry.target);
163                    render(entry.target);
164                }
165            });
166        }, { rootMargin: '300px' });
167        Array.prototype.forEach.call(maps, function (el) { watcher.observe(el); });
168    }
169
170    if (document.readyState === 'loading') {
171        document.addEventListener('DOMContentLoaded', init);
172    } else {
173        init();
174    }
175    // карты могут приезжать вместе с содержимым всплывающих окон
176    window.vvMapInit = init;
177})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.