PageSourceSearch

https://bit.plus/js/mapa.js

js bit.plus collected 2026-10-08 20:04:30 UTC 6,770 bytes, 215 lines download raw bytes

1/*
2  Store locator widget pro wBTCb.cz
3  ─────────────────────────────────
4  Automaticky načítá:
5  - Mapy.cz API klíč z /api/mapy-key (z env proměnné nastavené v Cloudflare)
6  - Prodejny GECO z /api/prodejny (z KV napumpovaného Workerem geco-sync)
7  - Vlastní BTM ze statického /btm.json
8
9  Vyžaduje Leaflet + Leaflet.markercluster (načítají se z CDN v <head>).
10*/
11(function () {
12  "use strict";
13
14  const URL_KEY      = "/api/mapy-key";
15  const URL_PRODEJNY = "/api/prodejny";
16  const URL_BTM      = "/btm.json";
17
18  // i18n fallback – pokud stránka nemá window.LANG/I18N, použijí se defaulty
19  function t(key, fallback) {
20    try {
21      if (window.I18N && window.LANG && window.I18N[window.LANG] && window.I18N[window.LANG][key]) {
22        return window.I18N[window.LANG][key];
23      }
24    } catch (_) {}
25    return fallback;
26  }
27
28  let map = null;
29  let clGeco = null;
30  let clBtm = null;
31  let loaded = false;
32  let apiKey = "";
33
34  function esc(s) {
35    return String(s == null ? "" : s).replace(/[&<>"']/g, c =>
36      ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;" }[c]));
37  }
38
39  function makeIcon(kind) {
40    return L.divIcon({
41      className: "",
42      html: `<div class="mmap-pin ${kind}"></div>`,
43      iconSize: [22, 22],
44      iconAnchor: [11, 22],
45      popupAnchor: [0, -22],
46    });
47  }
48
49  function popupProdejna(p) {
50    return `<div class="mmap-pop-title">${esc(p.name)}</div>
51      <div class="mmap-pop-addr">${esc(p.street)}<br>${esc(p.zip)} ${esc(p.city)}</div>
52      ${p.phone ? `<div class="mmap-pop-tel"><a href="tel:${esc(p.phone)}">${esc(p.phone)}</a></div>` : ""}`;
53  }
54
55  function popupBtm(b) {
56    const note    = b.note    ? `<div class="mmap-pop-note mmap-lang-cs">${esc(b.note)}</div>` : "";
57    const note_en = b.note_en ? `<div class="mmap-pop-note mmap-lang-en">${esc(b.note_en)}</div>` : "";
58    return `<div class="mmap-pop-title">${esc(b.name || "BTM")}</div>
59      ${b.address ? `<div class="mmap-pop-addr">${esc(b.address)}</div>` : ""}
60      ${note}${note_en}`;
61  }
62
63  function showError(msg) {
64    const container = document.getElementById("mmap");
65    if (!container) return;
66    container.innerHTML = `
67      <div id="mmap-loading">
68        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 9v4M12 17h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
69        <span>${esc(msg)}</span>
70      </div>`;
71  }
72
73  async function initMap() {
74    const container = document.getElementById("mmap");
75    if (!container || !window.L) return;
76
77    // Načteme API klíč nejdřív – bez něj nejede ani dlaždice
78    try {
79      const r = await fetch(URL_KEY);
80      const data = await r.json();
81      apiKey = data.key || "";
82    } catch (e) {
83      console.error("mmap: failed to load api key", e);
84    }
85
86    if (!apiKey) {
87      showError(t("map_err_key", "Mapa není nakonfigurovaná. Nastavte MAPY_API_KEY v Cloudflare."));
88      return;
89    }
90
91    // Vyprázdnit loading state a postavit kontejner pro Leaflet
92    container.innerHTML = `
93      <div id="mmap-canvas" style="width:100%;height:100%;"></div>
94
95    `;
96
97    map = L.map("mmap-canvas", {
98      preferCanvas: true,
99      zoomControl: true,
100      attributionControl: true,
101    }).setView([49.8, 15.5], 7);
102    // Odstraní Leaflet prefix (vlajka je součástí Leaflet prefixu, ne Mapy.com)
103    map.attributionControl.setPrefix('');
104
105    L.tileLayer(
106      `https://api.mapy.cz/v1/maptiles/basic/256/{z}/{x}/{y}?apikey=${apiKey}`,
107      {
108        minZoom: 0,
109        maxZoom: 19,
110        attribution: '<a href="https://mapy.com/" target="_blank">© Mapy.com</a>',
111        crossOrigin: true,
112      }
113    ).addTo(map);
114
115    clGeco = L.markerClusterGroup({ showCoverageOnHover: false, maxClusterRadius: 50 });
116    clBtm  = L.markerClusterGroup({ showCoverageOnHover: false, maxClusterRadius: 50 });
117
118    loadData();
119    // Attribution odkazy v nové záložce
120    setTimeout(function() {
121      document.querySelectorAll('.leaflet-control-attribution a').forEach(function(a) {
122        a.target = '_blank';
123        a.rel = 'noopener';
124      });
125    }, 500);
126
127  }
128
129  async function loadData() {
130    const iconGeco = makeIcon("geco");
131    const iconBtm  = makeIcon("btm");
132
133    let prodejny = [], btm = [];
134    try {
135      const [rP, rB] = await Promise.all([
136        fetch(URL_PRODEJNY).then(r => r.ok ? r.json() : []),
137        fetch(URL_BTM).then(r => r.ok ? r.json() : []),
138      ]);
139      prodejny = Array.isArray(rP) ? rP : [];
140      btm = Array.isArray(rB) ? rB : [];
141    } catch (e) {
142      console.error("mmap: data load failed", e);
143    }
144
145    let pCount = 0, bCount = 0;
146
147    for (const p of prodejny) {
148      if (typeof p.lat !== "number" || typeof p.lng !== "number") continue;
149      L.marker([p.lat, p.lng], { icon: iconGeco })
150        .bindPopup(popupProdejna(p))
151        .addTo(clGeco);
152      pCount++;
153    }
154    for (const b of btm) {
155      if (typeof b.lat !== "number" || typeof b.lng !== "number") continue;
156      L.marker([b.lat, b.lng], { icon: iconBtm })
157        .bindPopup(popupBtm(b))
158        .addTo(clBtm);
159      bCount++;
160    }
161
162    loaded = true;
163    applyFilter("all");
164
165
166  }
167
168  function applyFilter(which) {
169    if (!map || !loaded) return;
170    if (map.hasLayer(clGeco)) map.removeLayer(clGeco);
171    if (map.hasLayer(clBtm))  map.removeLayer(clBtm);
172    if (which === "all" || which === "geco") map.addLayer(clGeco);
173    if (which === "all" || which === "btm")  map.addLayer(clBtm);
174  }
175
176  // Globální mf() – volá se z HTML onclick="mf(this, 'all|btm|geco')"
177  // Přepíná aktivní stav tlačítek a filtruje mapu.
178  window.mf = function (btn, which) {
179    document.querySelectorAll(".mf").forEach(b => b.classList.remove("on"));
180    btn.classList.add("on");
181    applyFilter(which || "all");
182  };
183
184  // Lazy-init: mapu inicializuj, až když sekce vstoupí do viewportu.
185  function setupLazyInit() {
186    const el = document.getElementById("mmap");
187    if (!el) return;
188
189    function whenReady(cb) {
190      if (window.L && window.L.markerClusterGroup) { cb(); return; }
191      setTimeout(() => whenReady(cb), 50);
192    }
193
194    if ("IntersectionObserver" in window) {
195      const io = new IntersectionObserver((entries) => {
196        for (const e of entries) {
197          if (e.isIntersecting) {
198            io.disconnect();
199            whenReady(initMap);
200            return;
201          }
202        }
203      }, { rootMargin: "200px" });
204      io.observe(el);
205    } else {
206      whenReady(initMap);
207    }
208  }
209
210  if (document.readyState === "loading") {
211    document.addEventListener("DOMContentLoaded", setupLazyInit);
212  } else {
213    setupLazyInit();
214  }
215})();

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.