PageSourceSearch

https://www.daop.it/assets/js/daop-vicino.js

js daop.it collected 2026-10-02 01:03:40 UTC 17,171 bytes, 382 lines download raw bytes

1/* DAOP - "Vicino a me": filtro per distanza.
2 *
3 * Perche' esiste come file esterno: la funzione e' nata dentro eventi.html il
4 * 15/08/2026, ma il JS in fondo a quella pagina non passa da _guscio() (che
5 * ricopia solo <style>, nav e footer), quindi le pagine generate non lo
6 * vedevano. Ricopiarlo in quattordici template sarebbe stato quattordici copie
7 * da tenere allineate: e' la stessa ragione per cui daop-track.js e'
8 * un file solo.
9 *
10 * Perche' il filtro esiste: la tendina "provincia" non risponde a "vicino".
11 * Misurato da Alessandria, dei 128 eventi in provincia di AL la mediana sta a
12 * 25 km e il massimo a 55, e 14 eventi entro 25 km sono in provincia di Asti.
13 * La provincia include il lontano ed esclude il vicino.
14 *
15 * Il modulo non sa come si nasconde una riga: l'agenda usa una classe, le
16 * pagine di intenzione l'attributo `hidden`. Espone `entro(voce)` e la pagina
17 * lo somma ai propri filtri. E non manda niente a GA4: emette un evento DOM
18 * `daop:vicino`, che daop-track.js raccoglie - cosi' gtag resta scritto in un
19 * posto solo, come cookie-consent.js e' l'unico posto in cui GA4 si
20 * inizializza.
21 */
22(function () {
23  'use strict';
24
25  /* I gradini sono fissi e non uno slider: su un telefono uno slider e'
26     impreciso e i risultati saltano comunque a scatti. Ognuno porta il suo
27     conteggio, perche' scegliere fra "20 km" e "30 km" senza sapere che sono
28     26 e 135 eventi e' scegliere al buio. */
29  var STEPS = [10, 20, 30, 50];
30
31  /* Stessa soglia di MIN_FILTRI nel generatore: sotto questo numero di voci si
32     scorre prima la lista che a decidere in un controllo. */
33  var MIN_VOCI = 12;
34
35  /* Il gradino di partenza e' il primo che ha almeno tanti eventi: da Cuneo
36     entro 10 km ce ne sono 4, e una pagina che nasce vuota e' il difetto
37     tipico di questi filtri nelle zone rade. */
38  var SOGLIA_PARTENZA = 5;
39
40  function distanza(la1, lo1, la2, lo2) {
41    var R = 6371, r = Math.PI / 180;
42    var dLa = (la2 - la1) * r, dLo = (lo2 - lo1) * r;
43    var a = Math.sin(dLa / 2) * Math.sin(dLa / 2) +
44      Math.cos(la1 * r) * Math.cos(la2 * r) * Math.sin(dLo / 2) * Math.sin(dLo / 2);
45    return 2 * R * Math.asin(Math.sqrt(a));
46  }
47
48  function norm(s) {
49    return (s || '').toLowerCase().normalize('NFD')
50      .replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
51  }
52
53  function scriviKm(d) {
54    return d < 1 ? 'meno di 1 km' : Math.round(d) + ' km';
55  }
56
57  window.daopVicino = {
58    /* opz.voci      elementi con data-lat/data-lon (righe o schede)
59       opz.alCambio  la pagina rifa' i suoi filtri
60       opz.riga      dove appendere la distanza dentro una voce (opzionale)
61       opz.nomi      [singolare, plurale] di quello che la pagina elenca:
62                     ['evento', 'eventi'] se manca. Su luoghi.html sono
63                     ['luogo', 'luoghi'], e le due frasi che il modulo scrive
64                     da se' (il suggerimento a zero risultati e il comune che
65                     non c'e') devono dire la cosa giusta.
66       opz.dove      dove sta l'elenco, nella frase del comune che non c'e':
67                     'in agenda' se manca.
68       Torna null se il controllo non si e' acceso: la pagina prosegue senza. */
69    avvia: function (opz) {
70      var nodo = document.getElementById('ev-geo');
71      if (!nodo) return null;
72
73      var voci = (opz.voci || []).filter(function (v) {
74        return v.getAttribute('data-lat') && v.getAttribute('data-lon');
75      });
76      if (voci.length < MIN_VOCI) return null;
77
78      var go = document.getElementById('ev-geo-go');
79      var alt = document.getElementById('ev-geo-alt');
80      var campo = document.getElementById('ev-geo-q');
81      var lista = document.getElementById('ev-geo-list');
82      var da = document.getElementById('ev-geo-from');
83      var chips = document.getElementById('ev-geo-chips');
84      var azzera = document.getElementById('ev-geo-clear');
85      var hint = document.getElementById('ev-geo-hint');
86      var nota = document.getElementById('ev-geo-note');
87
88      var nomi = opz.nomi || ['evento', 'eventi'];
89      var dove = opz.dove || 'in agenda';
90      var centro = null, raggio = null, comuni = null, richiesta = 0;
91      var dist = new Map();
92      var alCambio = opz.alCambio || function () {};
93
94      function segnala(quale, extra) {
95        var d = { metodo: quale, raggio: raggio };
96        for (var k in extra) d[k] = extra[k];
97        try {
98          document.dispatchEvent(new CustomEvent('daop:vicino', { detail: d }));
99        } catch (e) { /* browser antichi: il filtro funziona lo stesso */ }
100      }
101
102      /* Una voce senza coordinate non e' "lontana", e' sconosciuta: con un
103         raggio attivo resta fuori, perche' dire "e' a 5 km" senza saperlo e'
104         peggio che non mostrarla. */
105      function entro(voce) {
106        if (!centro || raggio === null) return true;
107        var d = dist.get(voce);
108        return d !== undefined && d <= raggio;
109      }
110
111      function attivo() { return !!centro && raggio !== null; }
112
113      function impostaCentro(la, lo, etichetta, metodo) {
114        /* Scrivere un comune nel campo scatena `input` e poi `change`, cioe'
115           due volte lo stesso centro: senza questo si ricalcolavano le distanze
116           di tutte le voci due volte e partiva un evento doppio. */
117        if (centro && centro.la === la && centro.lo === lo) return;
118        centro = { la: la, lo: lo };
119        dist.clear();
120        voci.forEach(function (v) {
121          var d = distanza(la, lo, parseFloat(v.getAttribute('data-lat')),
122            parseFloat(v.getAttribute('data-lon')));
123          dist.set(v, d);
124          var dove = opz.riga ? opz.riga(v) : null;
125          if (!dove) return;
126          var tag = v.querySelector('.ev-km');
127          if (!tag) {
128            tag = document.createElement('span');
129            tag.className = 'ev-km';
130            dove.appendChild(tag);
131          }
132          tag.textContent = ' · a ' + scriviKm(d);
133        });
134        if (opz.alRitocco) opz.alRitocco();
135        var ordinate = [];
136        dist.forEach(function (d) { ordinate.push(d); });
137        raggio = null;
138        for (var i = 0; i < STEPS.length && raggio === null; i++) {
139          var n = 0;
140          for (var j = 0; j < ordinate.length; j++) { if (ordinate[j] <= STEPS[i]) n++; }
141          if (n >= SOGLIA_PARTENZA) raggio = STEPS[i];
142        }
143        if (raggio === null) raggio = STEPS[STEPS.length - 1];
144        da.textContent = 'da ' + etichetta;
145        /* Se il comune sta gia' scritto nel campo qui accanto, "da Acqui Terme"
146           lo ripete e basta: due etichette per la stessa cosa, su una riga che
147           sul telefono e' gia' stretta. Si stampa solo quando il posto non si
148           legge da nessun'altra parte, cioe' con il GPS. */
149        da.hidden = !campo.hidden && norm(campo.value) === norm(etichetta);
150        azzera.hidden = false;
151        /* Il campo del comune NON si chiude qui: la ricerca e' viva, e chi sta
152           ancora battendo "novi" ha gia' un centro su un prefisso. Chiudergli
153           il campo sotto le dita lo lascerebbe con un comune che non ha scelto
154           e senza il modo di correggerlo. */
155        if (go) go.hidden = true;
156        /* Con un centro impostato il ripiego non serve piu' e competerebbe con
157           l'etichetta del posto: la ✕ e' li' accanto e rimette tutto. */
158        if (alt) alt.hidden = true;
159        /* La promessa sulla posizione si fa solo quando una posizione e' stata
160           davvero letta: chi ha scelto un comune da un elenco non ha dato
161           niente, e rassicurarlo su un dato che non ha ceduto e' rumore - oltre
162           a costare due righe sul telefono. */
163        nota.textContent = metodo === 'gps'
164          ? 'Distanze in linea d\'aria. La posizione resta nel tuo browser.'
165          : 'Distanze in linea d\'aria dal centro di ' + etichetta + '.';
166        segnala(metodo);
167        alCambio();
168      }
169
170      function togliCentro(tieniAperto) {
171        centro = null; raggio = null; dist.clear();
172        voci.forEach(function (v) {
173          var t = v.querySelector('.ev-km');
174          if (t) t.parentNode.removeChild(t);
175        });
176        if (opz.alRitocco) opz.alRitocco();
177        da.hidden = true;
178        azzera.hidden = true;
179        chips.textContent = '';
180        hint.textContent = '';
181        if (!tieniAperto) {
182          campo.hidden = true; campo.value = '';
183          if (alt) alt.hidden = false;
184        }
185        if (go) { go.hidden = !gpsUsabile; go.disabled = false; go.textContent = '📍 Vicino a me'; }
186        nota.textContent = '';
187        alCambio();
188      }
189
190      /* Un gradino vuoto resta visibile ma spento: dice che li' non c'e'
191         niente, che e' una risposta, invece di sparire e far ballare la barra. */
192      function disegnaChip(per) {
193        STEPS.forEach(function (s, i) {
194          var b = chips.children[i];
195          if (!b) {
196            b = document.createElement('button');
197            b.type = 'button';
198            b.className = 'ev-geo-chip';
199            b.addEventListener('click', function () {
200              raggio = s;
201              segnala('gradino');
202              alCambio();
203            });
204            chips.appendChild(b);
205          }
206          b.textContent = s + ' km (' + per[i] + ')';
207          b.disabled = per[i] === 0;
208          b.setAttribute('aria-pressed', String(raggio === s));
209        });
210      }
211
212      /* La pagina passa il predicato dei SUOI filtri: i conteggi dei gradini
213         devono dire quanti eventi si vedrebbero davvero, non quanti ce ne sono
214         in totale a quella distanza. */
215      function conta(passaAltri) {
216        if (!centro) { chips.textContent = ''; hint.textContent = ''; return; }
217        var per = STEPS.map(function () { return 0; });
218        var dentro = 0;
219        voci.forEach(function (v) {
220          if (passaAltri && !passaAltri(v)) return;
221          var d = dist.get(v);
222          if (d === undefined) return;
223          STEPS.forEach(function (s, i) { if (d <= s) per[i]++; });
224          if (d <= raggio) dentro++;
225        });
226        disegnaChip(per);
227        /* A zero risultati si dice dove guardare. E' il difetto tipico dei
228           filtri per distanza nelle zone rade, e l'unica correzione e' non
229           lasciare il vuoto: da Cuneo entro 10 km ci sono 4 eventi, entro 30
230           quaranta. */
231        var prossimo = -1;
232        for (var i = 0; i < STEPS.length; i++) {
233          if (STEPS[i] > raggio && per[i] > 0) { prossimo = i; break; }
234        }
235        hint.textContent = (dentro === 0 && prossimo >= 0)
236          ? 'Niente entro ' + raggio + ' km. Entro ' + STEPS[prossimo] + ' km ' +
237            (per[prossimo] === 1 ? 'c\'è 1 ' + nomi[0] + '.' : 'ce ne sono ' + per[prossimo] + '.')
238          : '';
239      }
240
241      /* L'elenco dei comuni si ricava dalle voci alla prima apertura: un
242         <datalist> generato sarebbe HTML in pagina per tutti e utile a pochi.
243         Il punto di un comune e' la media dei suoi eventi - per una misura in
244         linea d'aria basta e avanza. */
245      function elencoComuni() {
246        if (comuni) return comuni;
247        var m = new Map();
248        voci.forEach(function (v) {
249          var n = v.getAttribute('data-citta');
250          if (!n) return;
251          var k = norm(n);
252          if (!m.has(k)) m.set(k, { nome: n, la: 0, lo: 0, n: 0, k: k });
253          var o = m.get(k);
254          o.la += parseFloat(v.getAttribute('data-lat'));
255          o.lo += parseFloat(v.getAttribute('data-lon'));
256          o.n++;
257        });
258        comuni = [];
259        m.forEach(function (o) {
260          comuni.push({ nome: o.nome, la: o.la / o.n, lo: o.lo / o.n, k: o.k });
261        });
262        comuni.sort(function (a, b) { return a.nome.localeCompare(b.nome, 'it'); });
263        comuni.forEach(function (c) {
264          var o = document.createElement('option');
265          o.value = c.nome;
266          lista.appendChild(o);
267        });
268        return comuni;
269      }
270
271      function apriComune() {
272        elencoComuni();
273        campo.hidden = false;
274        if (alt) alt.hidden = true;
275        campo.focus();
276      }
277
278      function provaComune() {
279        var t = norm(campo.value);
280        /* Svuotare il campo e' il modo naturale di disdire: lasciare "da Novi
281           Ligure" acceso sopra una casella vuota e' uno stato che non si
282           spiega. */
283        if (!t) { if (centro) togliCentro(true); return; }
284        var l = elencoComuni(), hit = null, i;
285        for (i = 0; i < l.length && !hit; i++) { if (l[i].k === t) hit = l[i]; }
286        if (!hit && t.length >= 3) {
287          for (i = 0; i < l.length && !hit; i++) { if (l[i].k.indexOf(t) === 0) hit = l[i]; }
288          for (i = 0; i < l.length && !hit; i++) { if (l[i].k.indexOf(t) > -1) hit = l[i]; }
289        }
290        if (hit) { impostaCentro(hit.la, hit.lo, hit.nome, 'comune'); return; }
291        /* Il catalogo dei comuni e' quello della pagina, non l'anagrafe: un
292           paese senza eventi in programma semplicemente non c'e'. Dirlo, invece
293           di lasciare la casella muta, e' la differenza fra una risposta e un
294           guasto. */
295        if (t.length >= 3) {
296          if (centro) togliCentro(true);
297          nota.textContent = 'Nessun ' + nomi[0] + ' ' + dove + ' a «' + campo.value.trim() +
298            '». Prova un comune vicino.';
299        }
300      }
301
302      function riposa() {
303        go.disabled = false;
304        go.textContent = '📍 Vicino a me';
305      }
306
307      function chiediPosizione() {
308        var mio = ++richiesta;
309        go.disabled = true;
310        go.textContent = '📍 Cerco…';
311        nota.textContent = '';
312        /* La sveglia e' nostra ed e' necessaria: il `timeout` della Geolocation
313           API misura solo il tempo per AGGANCIARE la posizione, non l'attesa
314           del permesso. Se l'avviso del browser resta li' senza risposta - che
315           e' il caso piu' comune, non un caso limite - non arriva ne' successo
316           ne' errore, e il bottone resterebbe "Cerco…" per sempre. Provato. */
317        var sveglia = setTimeout(function () {
318          if (mio !== richiesta || centro) return;
319          riposa();
320          nota.textContent = 'Non abbiamo ancora la posizione. Intanto puoi scegliere un comune da cui partire.';
321          apriComune();
322        }, 10000);
323        navigator.geolocation.getCurrentPosition(
324          function (pos) {
325            clearTimeout(sveglia);
326            /* Un "consenti" arrivato tardi vale lo stesso: la risposta buona
327               non si butta perche' la sveglia era gia' suonata. */
328            if (mio === richiesta) {
329              impostaCentro(pos.coords.latitude, pos.coords.longitude, 'dove sei', 'gps');
330            }
331          },
332          function (err) {
333            clearTimeout(sveglia);
334            if (mio !== richiesta) return;
335            riposa();
336            nota.textContent = (err && err.code === 1)
337              ? 'Non abbiamo la tua posizione. Puoi scegliere un comune da cui partire.'
338              : 'Non siamo riusciti a trovare la posizione. Puoi scegliere un comune da cui partire.';
339            apriComune();
340          },
341          /* enableHighAccuracy resta false: il gradino piu' stretto e' 10 km,
342             quindi un aggancio GPS non aggiunge niente e costa attesa e
343             batteria. E una posizione di cinque minuti fa va benissimo: le
344             sagre non si spostano. */
345          { enableHighAccuracy: false, timeout: 8000, maximumAge: 300000 });
346      }
347
348      var gpsUsabile = !!(navigator.geolocation && window.isSecureContext);
349      if (go) {
350        go.hidden = !gpsUsabile;
351        go.addEventListener('click', chiediPosizione);
352      }
353      if (alt) alt.addEventListener('click', apriComune);
354      azzera.addEventListener('click', function () { togliCentro(false); });
355      campo.addEventListener('change', provaComune);
356      campo.addEventListener('input', provaComune);
357      nodo.hidden = false;
358
359      /* Se il permesso e' gia' stato negato il bottone non serve: chiederlo di
360         nuovo non riapre nessun avviso, il browser rifiuta e basta. Meglio
361         portare subito al ripiego che offrire un comando che non fa niente. */
362      if (go && navigator.permissions && navigator.permissions.query) {
363        navigator.permissions.query({ name: 'geolocation' }).then(function (p) {
364          if (p.state === 'denied' && !centro) { gpsUsabile = false; go.hidden = true; }
365        }).catch(function () {});
366      }
367
368      /* "Azzera i filtri" della pagina deve togliere anche il raggio: se no
369         il bottone che promette l'elenco intero lascia fuori tutto quello che
370         sta oltre i 20 km, e la sola traccia del perche' e' una riga sopra. */
371      function spegni() { if (centro) togliCentro(false); }
372
373      /* La distanza di una voce dal centro, in km; undefined senza centro o
374         senza coordinate. Serve a chi ordina per vicinanza (luoghi.html): il
375         modulo misura e basta, l'ordine resta una decisione della pagina. */
376      function distanzaDi(voce) { return centro ? dist.get(voce) : undefined; }
377
378      return { entro: entro, attivo: attivo, conta: conta, azzera: spegni,
379               distanza: distanzaDi };
380    }
381  };
382})();

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.