PageSourceSearch

http://cityscanner.ru/assets/js/main.js

js cityscanner.ru collected 2026-10-02 06:34:25 UTC 9,428 bytes, 210 lines download raw bytes

1(function () {
2  'use strict';
3
4  var root = document.documentElement;
5
6  // ---------------------------------------------------------------- язык
7  var meta = {
8    ru: {
9      title: 'Ситисканнер — панорамы улиц для Яндекс Карт',
10      description: 'ООО «Ситисканнер» с 2009 года снимает панорамы улиц для Яндекс Карт: более 300 городов России и ближнего зарубежья, более 2 миллионов панорам.'
11    },
12    en: {
13      title: 'CityScanner — street-level panoramas for Yandex Maps',
14      description: 'CityScanner LLC has been capturing street-level panoramas for Yandex Maps since 2009: 300+ cities in Russia and neighbouring countries, over 2 million panoramas.'
15    }
16  };
17
18  function applyMeta(lang) {
19    document.title = meta[lang].title;
20    var d = document.querySelector('meta[name="description"]');
21    if (d) d.setAttribute('content', meta[lang].description);
22  }
23
24  function setLang(lang) {
25    root.lang = lang;
26    applyMeta(lang);
27    try { localStorage.setItem('lang', lang); } catch (e) {}
28    var url = new URL(location.href);
29    if (lang === 'ru') url.searchParams.delete('lang'); else url.searchParams.set('lang', lang);
30    history.replaceState(null, '', url);
31  }
32
33  applyMeta(root.lang);
34  document.querySelectorAll('[data-set-lang]').forEach(function (b) {
35    b.addEventListener('click', function () { setLang(b.getAttribute('data-set-lang')); });
36  });
37
38  // ---------------------------------------------------------------- меню
39  var burger = document.querySelector('.cs-header__menu');
40  var nav = document.getElementById('nav');
41  var header = document.querySelector('.cs-header');
42  function closeMenu() { nav.classList.remove('is-open'); burger.setAttribute('aria-expanded', 'false'); }
43  burger.addEventListener('click', function () {
44    var open = nav.classList.toggle('is-open');
45    burger.setAttribute('aria-expanded', String(open));
46  });
47  nav.addEventListener('click', function (e) { if (e.target.closest('a')) closeMenu(); });
48  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenu(); });
49
50  // тень у прилипшей шапки (shadow-sm) — только когда страница прокручена
51  function onScroll() { header.classList.toggle('is-stuck', window.scrollY > 4); }
52  window.addEventListener('scroll', onScroll, { passive: true });
53  onScroll();
54
55  // текущий раздел в меню — цвет accent и «точка съёмки» под пунктом, как в SiteHeader
56  if ('IntersectionObserver' in window) {
57    var links = {};
58    nav.querySelectorAll('a[href^="#"]').forEach(function (a) { links[a.getAttribute('href').slice(1)] = a; });
59    var visible = {};
60    var spy = new IntersectionObserver(function (entries) {
61      entries.forEach(function (e) { visible[e.target.id] = e.isIntersecting; });
62      var current = null;
63      Object.keys(links).forEach(function (id) { if (visible[id] && !current) current = id; });
64      Object.keys(links).forEach(function (id) {
65        if (id === current) links[id].setAttribute('aria-current', 'true');
66        else links[id].removeAttribute('aria-current');
67      });
68    }, { rootMargin: '-45% 0px -50% 0px' });
69    Object.keys(links).forEach(function (id) { var el = document.getElementById(id); if (el) spy.observe(el); });
70  }
71
72  document.querySelectorAll('.year').forEach(function (el) { el.textContent = new Date().getFullYear(); });
73
74  // ---------------------------------------------------------------- галерея
75  var box = document.getElementById('lightbox');
76  if (box && typeof box.showModal === 'function') {
77    var boxImg = box.querySelector('img');
78    document.querySelectorAll('.gallery__item').forEach(function (btn) {
79      btn.addEventListener('click', function () {
80        var img = btn.querySelector('img');
81        boxImg.src = btn.getAttribute('data-full');
82        boxImg.alt = img.alt;
83        box.showModal();
84      });
85    });
86    box.addEventListener('click', function (e) { if (e.target === box || e.target === boxImg) box.close(); });
87  } else {
88    document.querySelectorAll('.gallery__item').forEach(function (btn) {
89      btn.addEventListener('click', function () { window.open(btn.getAttribute('data-full'), '_blank'); });
90    });
91  }
92
93  // ---------------------------------------------------------------- панорама
94  // Тайлы готовит tools/build_assets.py (multires, куб 4608 px, тайл 512 px, 5 уровней)
95  function scene(name, yaw) {
96    return {
97      type: 'multires',
98      multiRes: {
99        basePath: 'assets/pano/' + name,
100        path: '/%l/%s%y_%x',
101        extension: 'jpg',
102        tileResolution: 512,
103        maxLevel: 5,
104        cubeResolution: 4608
105      },
106      yaw: yaw,
107      pitch: -4
108    };
109  }
110
111  var viewerEl = document.querySelector('.viewer');
112  var viewer = null;
113  var startHfov = 105;
114
115  function initViewer() {
116    if (viewer || !window.pannellum) return;
117    var en = root.lang === 'en';
118    // hfov в Pannellum — горизонтальный угол; на вертикальном экране берём уже, иначе сильные искажения
119    var portrait = viewerEl.clientWidth < viewerEl.clientHeight;
120    startHfov = portrait ? 70 : 105;
121    viewer = pannellum.viewer('pano', {
122      default: {
123        firstScene: 'p1',
124        sceneFadeDuration: 900,
125        autoLoad: true,
126        hfov: startHfov,
127        minHfov: 35,
128        maxHfov: 115,
129        mouseZoom: 'fullscreenonly',   // колесо мыши крутит страницу, а не зум
130        keyboardZoom: false,
131        compass: false,
132        showFullscreenCtrl: true,
133        strings: en ? {} : {
134          loadButtonLabel: 'Нажмите, чтобы<br>загрузить панораму',
135          loadingLabel: 'Загрузка…',
136          bylineLabel: '',
137          noPanoramaError: 'Панорама не указана.',
138          fileAccessError: 'Не удалось загрузить файл %s.',
139          malformedURLError: 'Некорректный адрес панорамы.',
140          iOS8WebGLError: 'Ваш браузер не поддерживает WebGL.',
141          genericWebGLError: 'Ваш браузер не поддерживает WebGL.',
142          textureSizeError: 'Панорама слишком велика для вашего устройства.',
143          unknownError: 'Неизвестная ошибка.'
144        }
145      },
146      scenes: {
147        p1: scene('p1', 0),
148        p2: scene('p2', 0),
149        p3: scene('p3', 0),
150        p4: scene('p4', 0)
151      }
152    });
153
154    ['mousedown', 'touchstart', 'pointerdown', 'keydown'].forEach(function (ev) {
155      viewerEl.addEventListener(ev, function () { viewerEl.classList.add('is-touched'); }, { once: true, passive: true });
156    });
157
158    initAutoRotate();
159
160    viewer.on('scenechange', function (id) {
161      document.querySelectorAll('.scene').forEach(function (b) {
162        b.classList.toggle('is-active', b.getAttribute('data-scene') === id);
163      });
164    });
165  }
166
167  // Автовращение ведём сами, а не через autoRotate Pannellum: если панорама крутится,
168  // пока её не видно, при возврате на экран браузер подвисает на 0,3–0,5 с.
169  // Поэтому крутим, только когда панорама на экране и страница не прокручивается.
170  function initAutoRotate() {
171    if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
172    var visible = false, timer = null;
173
174    function schedule(delay) {
175      clearTimeout(timer);
176      viewer.stopAutoRotate();
177      if (visible) timer = setTimeout(function () { viewer.startAutoRotate(-2.5, viewer.getPitch()); }, delay);
178    }
179
180    new IntersectionObserver(function (entries) {
181      visible = entries[entries.length - 1].isIntersecting;
182      schedule(1500);
183    }).observe(viewerEl);
184    window.addEventListener('scroll', function () { if (visible) schedule(1500); }, { passive: true });
185    // после того как посетитель сам покрутил панораму, даём ему спокойно посмотреть
186    ['pointerdown', 'touchstart', 'keydown'].forEach(function (ev) {
187      viewerEl.addEventListener(ev, function () { schedule(6000); }, { passive: true });
188    });
189    viewer.on('load', function () { schedule(1500); });
190  }
191
192  document.querySelectorAll('.scene').forEach(function (b) {
193    b.addEventListener('click', function (e) {
194      e.stopPropagation();
195      initViewer();
196      var id = b.getAttribute('data-scene');
197      if (viewer && viewer.getScene() !== id) viewer.loadScene(id, 0, -4, startHfov);
198    });
199  });
200
201  // панораму грузим, только когда до неё докрутили
202  function lazy() {
203    if (!('IntersectionObserver' in window)) return initViewer();
204    var io = new IntersectionObserver(function (entries) {
205      if (entries.some(function (e) { return e.isIntersecting; })) { io.disconnect(); initViewer(); }
206    }, { rootMargin: '300px 0px' });
207    io.observe(viewerEl);
208  }
209  if (window.pannellum) lazy(); else window.addEventListener('load', lazy);
210})();

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.