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.