1(function(){ 2 3 // ========= VARIABILI GLOBALI FILE ========= 4 let selectedFiles = []; 5 6const SPECIES_LIST = [ 7 "Specie non identificata", 8 "Airone bianco maggiore (Ardea alba)", 9 "Airone cenerino (Ardea cinerea)", 10 "Airone guardabuoi (Bubulcus ibis)", 11 "Airone rosso (Ardea purpurea)", 12 "Allocco (Strix aluco)", 13 "Allodola (Alauda arvensis)", 14 "Anatra germanata (Anas platyrhynchos)", 15 "Anatra mandarina (Aix galericulata)", 16 "Anatra muta (Cairina moschata)", 17 "Assiolo (Otus scops)", 18 "Astore (Accipiter gentilis)", 19 "Averla maggiore (Lanius excubitor)", 20 "Averla piccola (Lanius collurio)", 21 "Balestruccio (Delichon urbicum)", 22 "Balia nera (Ficedula hypoleuca)", 23 "Ballerina bianca (Motacilla alba)", 24 "Ballerina gialla (Motacilla cinerea)", 25 "Barbagianni (Tyto alba)", 26 "Beccaccia (Scolopax rusticola)", 27 "Beccaccino (Gallinago gallinago)", 28 "Beccafico (Sylvia borin)", 29 "Canapino (Hippolais polyglotta)", 30 "Capinera (Sylvia atricapilla)", 31 "Cardellino (Carduelis carduelis)", 32 "Cavaliere dâItalia (Himantopus himantopus)", 33 "Cesena (Turdus pilaris)", 34 "Cicogna bianca (Ciconia ciconia)", 35 "Cicogna nera (Ciconia nigra)", 36 "Cigno reale (Cygnus olor)", 37 "Cincia bigia (Poecile palustris)", 38 "Cincia dal ciuffo (Lophophanes cristatus)", 39 "Cincia mora (Periparus ater)", 40 "Cinciallegra (Parus major)", 41 "Cinciarella (Cyanistes caeruleus)", 42 "Civetta (Athene noctua)", 43 "Codibugnolo (Aegithalos caudatus)", 44 "Codirosso (Phoenicurus phoenicurus)", 45 "Codirosso spazzacamino (Phoenicurus ochruros)", 46 "Colombaccio (Columba palumbus)", 47 "Cormorano (Phalacrocorax carbo)", 48 "Cornacchia grigia (Corvus cornix)", 49 "Cornacchia nera (Corvus corone)", 50 "Corriere grosso (Charadrius hiaticula)", 51 "Corriere piccolo (Charadrius dubius)", 52 "Cuculo (Cuculus canorus)", 53 "Cutrettola (Motacilla flava)", 54 "Fagiano (Phasianus colchicus)", 55 "Falco di palude (Circus aeruginosus)", 56 "Falco pellegrino (Falco peregrinus)", 57 "Falco pescatore (Pandion haliaetus)", 58 "Fenicottero (Phoenicopterus roseus)", 59 "Fiorrancino (Regulus ignicapilla)", 60 "Fischione (Mareca penelope)", 61 "Folaga (Fulica atra)", 62 "Fringuello (Fringilla coelebs)", 63 "Frosone (Coccothraustes coccothraustes)", 64 "Gabbiano comune (Chroicocephalus ridibundus)", 65 "Gabbiano reale (Larus michahellis)", 66 "Gallinella dâacqua (Gallinula chloropus)", 67 "Garzetta (Egretta garzetta)", 68 "Gavina (Larus canus)", 69 "Gazza (Pica pica)", 70 "Germano reale (Anas platyrhynchos)", 71 "Gheppio (Falco tinnunculus)", 72 "Ghiandaia (Garrulus glandarius)", 73 "Gru (Grus grus)", 74 "Gruccione (Merops apiaster)", 75 "Gufo comune (Asio otus)", 76 "Ibis sacro (Threskiornis aethiopicus)", 77 "Lodolaio (Falco subbuteo)", 78 "Lucherino (Spinus spinus)", 79 "Luì bianco (Phylloscopus bonelli)", 80 "Luì grosso (Phylloscopus trochilus)", 81 "Luì piccolo (Phylloscopus collybita)", 82 "Luì verde (Phylloscopus sibilatrix)", 83 "Marangone minore (Microcarbo pygmaeus)", 84 "Martin pescatore (Alcedo atthis)", 85 "Marzaiola (Spatula querquedula)", 86 "Merlo (Turdus merula)", 87 "Mestolone (Spatula clypeata)", 88 "Migliarino di palude (Emberiza schoeniclus)", 89 "Mignattaio (Plegadis falcinellus)", 90 "Moretta (Aythya fuligula)", 91 "Moretta tabaccata (Aythya nyroca)", 92 "Moriglione (Aythya ferina)", 93 "Nibbio bruno (Milvus migrans)", 94 "Nitticora (Nycticorax nycticorax)", 95 "Oca selvatica (Anser anser)", 96 "Parrocchetto dal collare (Psittacula krameri)", 97 "Parrocchetto monaco (Myiopsitta monachus)", 98 "Passera dâItalia (Passer italiae)", 99 "Passera mattugia (Passer montanus)", 100 "Passera scopaiola (Prunella modularis)", 101 "Pavoncella (Vanellus vanellus)", 102 "Pendolino (Remiz pendulinus)", 103 "Pernice rossa (Alectoris rufa)", 104 "Pettirosso (Erithacus rubecula)", 105 "Piccione di città (Columba livia)", 106 "Picchio muratore (Sitta europaea)", 107 "Picchio rosso maggiore (Dendrocopos major)", 108 "Picchio rosso minore (Dryobates minor)", 109 "Picchio verde (Picus viridis)", 110 "Pigliamosche (Muscicapa striata)", 111 "Piro piro piccolo (Actitis hypoleucos)", 112 "Pispola (Anthus pratensis)", 113 "Poiana (Buteo buteo)", 114 "Quaglia (Coturnix coturnix)", 115 "Regolo (Regulus regulus)", 116 "Rigogolo (Oriolus oriolus)", 117 "Rondine (Hirundo rustica)", 118 "Rondine montana (Ptyonoprogne rupestris)", 119 "Rondone comune (Apus apus)", 120 "Rondone maggiore (Tachymarptis melba)",
121 "Rondone pallido (Apus pallidus)", 122 "Saltimpalo (Saxicola rubicola)", 123 "Scricciolo (Troglodytes troglodytes)", 124 "Sgarza ciuffetto (Ardeola ralloides)", 125 "Smergo maggiore (Mergus merganser)", 126 "Sparviere (Accipiter nisus)", 127 "Spioncello (Anthus spinoletta)", 128 "Sterna comune (Sterna hirundo)", 129 "Stiaccino (Saxicola rubetra)", 130 "Storno (Sturnus vulgaris)", 131 "Succiacapre (Caprimulgus europaeus)", 132 "Svasso maggiore (Podiceps cristatus)", 133 "Taccola (Coloeus monedula)", 134 "Tarabuso (Botaurus stellaris)", 135 "Tarabusino (Ixobrychus minutus)", 136 "Topino (Riparia riparia)", 137 "Torcicollo (Jynx torquilla)", 138 "Tordo bottaccio (Turdus philomelos)", 139 "Tortora dal collare (Streptopelia decaocto)", 140 "Tortora selvatica (Streptopelia turtur)", 141 "Tuffetto (Tachybaptus ruficollis)", 142 "Upupa (Upupa epops)", 143 "Usignolo (Luscinia megarhynchos)", 144 "Usignolo di fiume (Cettia cetti)", 145 "Verdone (Chloris chloris)", 146 "Verzellino (Serinus serinus)" 147]; 148 149 150function setupAutocomplete(inp, arr) { 151 let currentFocus; 152 inp.addEventListener("input", function(e) { 153 let a, b, i, val = this.value; 154 closeAllLists(); 155 if (!val) { return false;} 156 currentFocus = -1; 157 a = document.createElement("DIV"); 158 a.setAttribute("id", this.id + "autocomplete-list"); 159 a.setAttribute("class", "autocomplete-items"); 160 this.parentNode.appendChild(a); 161 162 let matches = 0; 163 for (i = 0; i < arr.length; i++) { 164 if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) { 165 matches++; 166 if(matches > 10) break; 167 b = document.createElement("DIV"); 168 b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>"; 169 b.innerHTML += arr[i].substr(val.length); 170 b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>"; 171 b.addEventListener("click", function(e) { 172 inp.value = this.getElementsByTagName("input")[0].value; 173 closeAllLists(); 174 }); 175 a.appendChild(b); 176 } 177 } 178 }); 179 180 inp.addEventListener("keydown", function(e) { 181 let x = document.getElementById(this.id + "autocomplete-list"); 182 if (x) x = x.getElementsByTagName("div"); 183 if (e.keyCode == 40) { 184 currentFocus++; 185 addActive(x); 186 } else if (e.keyCode == 38) { 187 currentFocus--; 188 addActive(x); 189 } else if (e.keyCode == 13) { 190 e.preventDefault(); 191 if (currentFocus > -1) { 192 if (x) x[currentFocus].click(); 193 } 194 } 195 }); 196 197 function addActive(x) { 198 if (!x) return false; 199 removeActive(x); 200 if (currentFocus >= x.length) currentFocus = 0; 201 if (currentFocus < 0) currentFocus = (x.length - 1); 202 x[currentFocus].classList.add("autocomplete-active"); 203 } 204 205 function removeActive(x) { 206 for (let i = 0; i < x.length; i++) { 207 x[i].classList.remove("autocomplete-active"); 208 } 209 } 210 211 function closeAllLists(elmnt) { 212 const x = document.getElementsByClassName("autocomplete-items"); 213 for (let i = 0; i < x.length; i++) { 214 if (elmnt != x[i] && elmnt != inp) { 215 x[i].parentNode.removeChild(x[i]); 216 } 217 } 218 } 219 220 document.addEventListener("click", function (e) { 221 closeAllLists(e.target); 222 }); 223} 224 225 // Conterrà oggetti { file, id } 226 const MAX_FILES = 2; 227 const MAX_TOTAL_BYTES = 4 * 1024 * 1024; // 4MB per stare nei limiti Netlify (6MB payload) 228 229 const STORAGE_KEY_NOME = "guardaMI_uccelli_cittadini_nome"; 230 const STORAGE_KEY_EMAIL = "guardaMI_uccelli_cittadini_email"; 231 const AGE_GATE_KEY = "guardaMI_age_gate_14_or_parent"; 232 const SHARED_CITIZEN_PROFILE_KEY = "guardaMI_cittadini_profile_v1"; 233 234 function getSharedCitizenProfile() { 235 try { 236 const raw = localStorage.getItem(SHARED_CITIZEN_PROFILE_KEY); 237 if (!raw) return null; 238 const data = JSON.parse(raw); 239 if (!data || typeof data !== "object") return null; 240 const nome = typeof data.nome === "string" ? data.nome : ""; 241 const mail = typeof data.mail === "string" ? data.mail : ""; 242 return { nome, mail }; 243 } catch (e) { 244 return null; 245 } 246 } 247 248 function setSharedCitizenProfile(nome, mail) { 249 try { 250 const payload = { 251 nome: String(nome || ""), 252 mail: String(mail || ""), 253 updatedAt: Date.now() 254 }; 255 localStorage.setItem(SHARED_CITIZEN_PROFILE_KEY, JSON.stringify(payload)); 256 localStorage.setItem("userData", JSON.stringify({ nome: payload.nome, mail: payload.mail })); 257 } catch (e) {} 258 } 259 260 function clearSharedCitizenProfile() { 261 try { localStorage.removeItem(SHARED_CITIZEN_PROFILE_KEY); } catch (e) {} 262 try { localStorage.removeItem("userData"); } catch (e) {} 263 } 264 265 function loadUserDataFromStorage() { 266 const nomeInput = document.getElementById('nomeRilevatore'); 267 const emailInput = document.getElementById('email'); 268 269 if (typeof(Storage) !== "undefined") { 270 const shared = getSharedCitizenProfile(); 271 if (shared) { 272 if (nomeInput && !nomeInput.value && shared.nome) nomeInput.value = shared.nome; 273 if (emailInput && !emailInput.value && shared.mail) emailInput.value = shared.mail; 274 } 275 276 const legacyNameKeys = ["uccelli_cittadini_def_userName", "cittadini_user_name"]; 277 const legacyEmailKeys = ["uccelli_cittadini_def_userEmail", "cittadini_user_email"]; 278 279 if (!localStorage.getItem(STORAGE_KEY_NOME)) { 280 for (let i = 0; i < legacyNameKeys.length; i++) { 281 const v = localStorage.getItem(legacyNameKeys[i]); 282 if (v) { 283 localStorage.setItem(STORAGE_KEY_NOME, v); 284 break; 285 } 286 } 287 } 288 if (!localStorage.getItem(STORAGE_KEY_EMAIL)) { 289 for (let i = 0; i < legacyEmailKeys.length; i++) { 290 const v = localStorage.getItem(legacyEmailKeys[i]); 291 if (v) { 292 localStorage.setItem(STORAGE_KEY_EMAIL, v); 293 break; 294 } 295 } 296 } 297 298 if (localStorage.getItem(STORAGE_KEY_NOME) && nomeInput) { 299 if (!nomeInput.value) nomeInput.value = localStorage.getItem(STORAGE_KEY_NOME); 300 } 301 if (localStorage.getItem(STORAGE_KEY_EMAIL) && emailInput) { 302 if (!emailInput.value) emailInput.value = localStorage.getItem(STORAGE_KEY_EMAIL); 303 } 304 305 const currentShared = getSharedCitizenProfile(); 306 if (!currentShared) { 307 const n = nomeInput ? (nomeInput.value || "") : ""; 308 const m = emailInput ? (emailInput.value || "") : ""; 309 if (n || m) setSharedCitizenProfile(n, m); 310 } 311 } 312 } 313 314 function saveUserDataToStorage() { 315 const nomeInput = document.getElementById('nomeRilevatore'); 316 const emailInput = document.getElementById('email'); 317 318 if (typeof(Storage) !== "undefined") { 319 if (nomeInput) localStorage.setItem(STORAGE_KEY_NOME, nomeInput.value); 320 if (emailInput) localStorage.setItem(STORAGE_KEY_EMAIL, emailInput.value); 321 } 322 setSharedCitizenProfile(nomeInput ? nomeInput.value : "", emailInput ? emailInput.value : ""); 323 } 324 325 function clearUserDataFromStorageAndForm() { 326 const nomeInput = document.getElementById('nomeRilevatore'); 327 const emailInput = document.getElementById('email'); 328 329 if (confirm("Sei sicuro di voler cancellare i dati salvati (Nome, E-mail) da questo dispositivo?")) { 330 if (typeof(Storage) !== "undefined") { 331 localStorage.removeItem(STORAGE_KEY_NOME); 332 localStorage.removeItem(STORAGE_KEY_EMAIL); 333 localStorage.removeItem("uccelli_cittadini_def_userName"); 334 localStorage.removeItem("uccelli_cittadini_def_userEmail"); 335 localStorage.removeItem("cittadini_user_name"); 336 localStorage.removeItem("cittadini_user_email"); 337 } 338 clearSharedCitizenProfile(); 339 if (nomeInput) nomeInput.value = ""; 340 if (emailInput) emailInput.value = ""; 341 alert("I tuoi dati sono stati rimossi da questo dispositivo."); 342 } 343 } 344 345 // Init al caricamento 346 window.addEventListener('DOMContentLoaded', () => { 347 348 const nomeSpecieInput = document.getElementById("nomeSpecie"); 349 if (nomeSpecieInput) { 350 setupAutocomplete(nomeSpecieInput, SPECIES_LIST); 351 } 352 353 loadUserDataFromStorage(); 354 355 const nomeInput = document.getElementById('nomeRilevatore'); 356 const emailInput = document.getElementById('email'); 357 const clearBtn = document.getElementById('clearSavedUserData'); 358 const ageGate = document.getElementById('ageGate14'); 359 360 if (nomeInput) { 361 nomeInput.addEventListener('input', saveUserDataToStorage); 362 nomeInput.addEventListener('change', saveUserDataToStorage); 363 } 364 if (emailInput) { 365 emailInput.addEventListener('input', saveUserDataToStorage); 366 emailInput.addEventListener('change', saveUserDataToStorage); 367 } 368 if (clearBtn) { 369 clearBtn.addEventListener('click', clearUserDataFromStorageAndForm); 370 } 371 if (ageGate) { 372 if (localStorage.getItem(AGE_GATE_KEY) === "true") ageGate.checked = true; 373 ageGate.addEventListener('change', function() { 374 localStorage.setItem(AGE_GATE_KEY, ageGate.checked ? "true" : "false"); 375 if (typeof updateSubmitState === "function") updateSubmitState(); 376 }); 377 } 378 379 // Initialize Date to Today 380 const dateField = document.getElementById('dataAvvistamento'); 381 if (dateField && !dateField.value) { 382 const today = new Date(); 383 const yyyy = today.getFullYear(); 384 const mm = String(today.getMonth() + 1).padStart(2, '0'); 385 const dd = String(today.getDate()).padStart(2, '0'); 386 dateField.value = `${yyyy}-${mm}-${dd}`; 387 } 388 }); 389 390 391 // ========= GOOGLE MAPS + GEOLOCALIZZAZIONE ========= 392 let map; 393 let marker; 394 window.manualEditMade = false;
395 396 // ========= PERIMETRO CITTÃ METROPOLITANA DI MILANO ========= 397 let milanoMetroPolygons = null; 398 let milanoBoundaryPromise = null; 399 400 function loadMilanoBoundary() { 401 if (milanoBoundaryPromise) return milanoBoundaryPromise; 402 // Assumiamo che il file GeoJSON sia presente nella root del sito (come in ricercatori) 403 milanoBoundaryPromise = fetch("/Comuni_zone_omogenee_20260117.geojson") 404 .then(function(response) { 405 if (!response.ok) throw new Error("HTTP " + response.status); 406 return response.json(); 407 }) 408 .then(function(data) { 409 const features = data && data.features ? data.features : []; 410 const polygons = []; 411 for (let i = 0; i < features.length; i++) { 412 const geom = features[i] && features[i].geometry; 413 if (!geom || !geom.type || !geom.coordinates) continue; 414 if (geom.type === "MultiPolygon") { 415 const mp = geom.coordinates; 416 for (let j = 0; j < mp.length; j++) { 417 polygons.push(mp[j]); 418 } 419 } else if (geom.type === "Polygon") { 420 polygons.push(geom.coordinates); 421 } 422 } 423 milanoMetroPolygons = polygons; 424 console.log("Confine Milano caricato:", milanoMetroPolygons.length, "poligoni"); 425 }) 426 .catch(function(err) { 427 console.error("Errore nel caricamento del confine di Milano:", err); 428 milanoMetroPolygons = null; 429 }); 430 return milanoBoundaryPromise; 431 } 432 433 function ensureMilanoBoundaryLoaded() { 434 if (milanoMetroPolygons && Array.isArray(milanoMetroPolygons) && milanoMetroPolygons.length > 0) { 435 return Promise.resolve(); 436 } 437 return loadMilanoBoundary(); 438 } 439 440 function pointInRing(lat, lng, ring) { 441 let inside = false;
442 const y = lat; 443 const x = lng; 444 for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { 445 const xi = ring[i][0]; 446 const yi = ring[i][1]; 447 const xj = ring[j][0]; 448 const yj = ring[j][1]; 449 const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi); 450 if (intersect) inside = !inside; 451 } 452 return inside; 453 } 454 455 function pointInPolygon(lat, lng, polygon) { 456 if (!polygon || polygon.length === 0) return false; 457 if (!pointInRing(lat, lng, polygon[0])) return false; 458 for (let i = 1; i < polygon.length; i++) { 459 if (pointInRing(lat, lng, polygon[i])) return false; 460 } 461 return true; 462 } 463 464 function isInsideMilanoMetro(lat, lng) { 465 if (typeof lat !== "number" || typeof lng !== "number") return false; 466 if (!isFinite(lat) || !isFinite(lng)) return false; 467 468 // Bounding box di sicurezza (include San Colombano al Lambro) 469 const minLat = 45.10, maxLat = 45.70; 470 const minLng = 8.80, maxLng = 9.70; 471 const inBoundingBox = (lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng); 472 473 if (milanoMetroPolygons && Array.isArray(milanoMetroPolygons) && milanoMetroPolygons.length > 0) { 474 for (let i = 0; i < milanoMetroPolygons.length; i++) { 475 if (pointInPolygon(lat, lng, milanoMetroPolygons[i])) { 476 return true; 477 } 478 } 479 // Se siamo fuori dai poligoni ma dentro la bbox, è fuori (perché i poligoni sono precisi) 480 // Se non abbiamo poligoni caricati, usiamo la bbox come fallback 481 return false; 482 } 483 return inBoundingBox; 484 } 485 486 // Handler globale per errori di autenticazione Google Maps 487 window.gm_authFailure = function() { 488 const mapDiv = document.getElementById('map'); 489 if (mapDiv) { 490 mapDiv.innerHTML = '<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; padding:20px; text-align:center; color:#b91c1c; background:#fef2f2; border:1px solid #ef4444; border-radius:8px;"><div>â ï¸ <strong>Errore caricamento mappa</strong></div><div style="font-size:13px; margin-top:5px;">Verifica la chiave API o le impostazioni del dominio (Referrer) nella Google Cloud Console.</div></div>'; 491 mapDiv.style.height = '320px'; // Assicura altezza 492 } 493 console.error("Google Maps API Authentication Failure"); 494 }; 495 496 window.initMap = function() { 497 console.log("initMap called. Checking Google Maps API..."); 498 if (window.google && window.google.maps) { 499 console.log("Google Maps API loaded. Version:", google.maps.version); 500 } else { 501 console.error("Google Maps API NOT loaded."); 502 } 503 504 const mapDiv = document.getElementById('map'); 505 if (!mapDiv || !window.google || !google.maps) return; 506 507 // Centro di default: Milano 508 const defaultPos = { lat: 45.4642, lng: 9.19 }; 509 510 map = new google.maps.Map(mapDiv, { 511 center: defaultPos, 512 zoom: 12, 513 streetViewControl: false, 514 mapTypeControl: false 515 }); 516 517 marker = new google.maps.Marker({ 518 position: defaultPos, 519 map: map, 520 draggable: true 521 }); 522 523 // ========= SEARCH BOX (Autocomplete) ========= 524 const searchInput = document.getElementById('mapSearch'); 525 const jotformGeoInput = document.getElementById('jotformGeoloc'); 526 527 if (searchInput && google.maps.places) { 528 const autocomplete = new google.maps.places.Autocomplete(searchInput, { 529 fields: ['geometry', 'formatted_address'], 530 types: ['geocode'] 531 }); 532 533 autocomplete.bindTo('bounds', map); 534 535 autocomplete.addListener('place_changed', () => { 536 window.manualEditMade = true; // Utente ha cercato un indirizzo 537 const place = autocomplete.getPlace(); 538 if (!place.geometry || !place.geometry.location) { 539 return; 540 } 541 542 const location = place.geometry.location; 543 544 if (place.geometry.viewport) { 545 map.fitBounds(place.geometry.viewport); 546 } else { 547 map.setCenter(location); 548 map.setZoom(17); 549 } 550 551 marker.setPosition(location); 552 553 const lat = location.lat(); 554 const lng = location.lng(); 555 const latInput = document.getElementById('latitudine'); 556 const lngInput = document.getElementById('longitudine'); 557 558 if (latInput && lngInput) { 559 latInput.value = lat.toFixed(6); 560 lngInput.value = lng.toFixed(6); 561 } 562 563 if (jotformGeoInput) { 564 jotformGeoInput.value = lat.toFixed(6) + ', ' + lng.toFixed(6); 565 } 566 }); 567 568 searchInput.addEventListener('keydown', (e) => { 569 if (e.key === 'Enter') { 570 e.preventDefault(); 571 } 572 }); 573 } 574 575 window.map = map; 576 window.marker = marker; 577 578 const latInput = document.getElementById('latitudine'); 579 const lngInput = document.getElementById('longitudine'); 580 581 try { 582 const pending = window.__guardamiPendingExifCoords; 583 if (pending && typeof pending.lat === "number" && typeof pending.lng === "number") { 584 map.setCenter(pending); 585 map.setZoom(15); 586 marker.setPosition(pending); 587 if (latInput && lngInput) { 588 latInput.value = pending.lat.toFixed(6); 589 lngInput.value = pending.lng.toFixed(6); 590 } 591 syncGeoToJotform(); 592 window.manualEditMade = true; 593 window.__guardamiPendingExifCoords = null; 594 } 595 } catch (e) {} 596 597 // Inizializza i campi con la posizione di default se vuoti 598 syncGeoToJotform(); 599 600 // Sync marker -> inputs 601 marker.addListener('dragend', (e) => { 602 window.manualEditMade = true; 603 const pos = e.latLng; 604 if (latInput && lngInput) { 605 latInput.value = pos.lat().toFixed(6); 606 lngInput.value = pos.lng().toFixed(6); 607 } 608 syncGeoToJotform(); 609 610 const lat = pos.lat(); 611 const lng = pos.lng(); 612 const minLat = 45.10, maxLat = 45.70; 613 const minLng = 8.80, maxLng = 9.70; 614 const inBoundingBox = (lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng); 615 if (!inBoundingBox) { 616 alert("Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo è riservato alle osservazioni in questo territorio."); 617 } 618 }); 619 620 // Tentativo recupero sessione 621 /* DISABILITATO: Nessun dato salvato in locale. 622 try { 623 const last = sessionStorage.getItem('geoLast'); 624 if (last) { 625 const obj = JSON.parse(last); 626 if (obj && typeof obj.lat === 'number') { 627 const p = { lat: obj.lat, lng: obj.lng }; 628 map.setCenter(p); 629 map.setZoom(15); 630 marker.setPosition(p); 631 if (latInput) latInput.value = obj.lat.toFixed(6); 632 if (lngInput) lngInput.value = obj.lng.toFixed(6); 633 } 634 } 635 } catch(e) {} 636 */ 637 console.log("Recupero sessione geo disabilitato per privacy."); 638 639 // Avvio geolocalizzazione 640 chiediGeolocalizzazione(); 641 }; 642 643 window.chiediGeolocalizzazione = function() { 644 if (!navigator.geolocation || !map || !marker) return; 645 646 navigator.geolocation.getCurrentPosition( 647 (pos) => { 648 if (window.manualEditMade) return; 649 const lat = pos.coords.latitude; 650 const lng = pos.coords.longitude; 651 const nuovaPos = { lat, lng }; 652 653 map.setCenter(nuovaPos); 654 map.setZoom(15); 655 marker.setPosition(nuovaPos); 656 657 const latInput = document.getElementById('latitudine'); 658 const lngInput = document.getElementById('longitudine'); 659 if (latInput && lngInput) { 660 latInput.value = lat.toFixed(6); 661 lngInput.value = lng.toFixed(6); 662 } 663 syncGeoToJotform(); 664 /* DISABILITATO: Nessun dato salvato in locale. 665 try { sessionStorage.setItem('geoConsentSession','granted'); } catch(e) {} 666 */ 667 console.log("Salvataggio consenso geo disabilitato per privacy."); 668 }, 669 (err) => { 670 console.warn('Errore geolocalizzazione:', err); 671 }, 672 { enableHighAccuracy: true, timeout: 10000 } 673 ); 674 } 675 676 function syncGeoToJotform() { 677 const latInput = document.getElementById('latitudine'); 678 const lngInput = document.getElementById('longitudine'); 679 const jotformGeo = document.getElementById('jotformGeoloc'); 680 if (latInput && lngInput && jotformGeo) { 681 if (latInput.value && lngInput.value) { 682 jotformGeo.value = latInput.value + ', ' + lngInput.value; 683 } else { 684 jotformGeo.value = ''; 685 } 686 } 687 } 688 689 // ========= LOGICA UI (Conosci specie / Dimensione / Colori) ========= 690 const radios = document.querySelectorAll('input[name="conosciSpecieLocal"]'); 691 const species = document.getElementById('speciesFields'); 692 const colorPanel = document.getElementById('colorPanel'); 693 const dimensionePanel = document.getElementById('dimensionePanel'); 694 const dimRadios = document.querySelectorAll('input[name="dimensione"]'); 695 const sizeRefImage = document.getElementById('sizeRefImage'); 696 const colorBtns = document.querySelectorAll('.color-btn'); 697 698 // MAPPATURA SPECIE PER DIMENSIONE E COLORE 699 const speciesMappings = [ 700 // PICCOLO 701 { size: 'PICCOLO', color: 'BIANCO', species: ['balestruccio', 'codibugnolo', 'corriere-piccolo'] }, 702 { size: 'PICCOLO', color: 'BIANCO/NERO', species: ['balestruccio', 'codibugnolo', 'picchio-rosso-minore'] }, 703 { size: 'PICCOLO', color: 'NERO/GRIGIO', species: ['capinera', 'codirosso-spazzacamino'] }, 704 { size: 'PICCOLO', color: 'GRIGIO', species: ['capinera', 'pigliamosche', 'codirosso-spazzacamino'] }, 705 { size: 'PICCOLO', color: 'GIALLO', species: ['cinciarella', 'cinciallegra', 'verzellino', 'verdone', 'cardellino', 'lucherino', 'fiorrancino', 'regolo'] }, 706 { size: 'PICCOLO', color: 'GIALLO/BLU', species: ['cinciarella'] }, 707 { size: 'PICCOLO', color: 'BLU', species: ['cinciarella', 'martin-pescatore'] }, 708 { size: 'PICCOLO', color: 'ROSSO', species: ['picchio-rosso-minore', 'pettirosso', 'cardellino', 'codirosso-comune', 'codirosso-spazzacamino'] }, 709 { size: 'PICCOLO', color: 'ROSSO/GIALLO', species: ['cardellino'] }, 710 { size: 'PICCOLO', color: 'MARRONE/ROSSO', species: ['pettirosso', 'fringuello', 'codirosso-comune', 'peppola'] }, 711 { size: 'PICCOLO', color: 'VERDE', species: ['verdone', 'regolo'] }, 712 { size: 'PICCOLO', color: 'BEIGE', species: ['torcicollo', 'passera-scopaiola', 'usignolo', 'corriere-piccolo', 'passera-d-italia', 'fiorrancino', 'rondine-montana'] },
713 { size: 'PICCOLO', color: 'MARRONE', species: ['passera-d-italia', 'passera-mattugia', 'torcicollo', 'passera-scopaiola', 'corriere-piccolo'] }, 714 715 716 // MEDIO 717 { size: 'MEDIO', color: 'BIANCO', species: ['ballerina-bianca'] }, 718 { size: 'MEDIO', color: 'BIANCO/NERO', species: ['ballerina-bianca', 'rondine', 'pavoncella'] }, 719 { size: 'MEDIO', color: 'NERO/GRIGIO', species: [] }, 720 { size: 'MEDIO', color: 'GRIGIO', species: ['assiolo'] }, 721 { size: 'MEDIO', color: 'GIALLO', species: ['ballerina-gialla', 'gruccione', 'rigogolo'] }, 722 { size: 'MEDIO', color: 'GIALLO/BLU', species: [] }, 723 { size: 'MEDIO', color: 'BLU', species: ['gruccione'] }, 724 { size: 'MEDIO', color: 'ROSSO', species: ['picchio-rosso-maggiore'] }, 725 { size: 'MEDIO', color: 'ROSSO/GIALLO', species: [] }, 726 { size: 'MEDIO', color: 'MARRONE/ROSSO', species: ['tuffetto'] }, 727 { size: 'MEDIO', color: 'VERDE', species: ['storno'] }, 728 { size: 'MEDIO', color: 'BEIGE', species: ['civetta', 'rondone-pallido', 'rondone-maggiore', 'torcicollo', 'averla-piccola'] }, 729 { size: 'MEDIO', color: 'MARRONE', species: ['civetta', 'rondone-pallido', 'rondone-maggiore', 'merlo-femmina', 'assiolo', 'porciglione', 'tuffetto', 'averla-piccola'] }, 730 { size: 'MEDIO', color: 'NERO', species: ['merlo', 'storno', 'rondone-comune'] }, 731 732 // GRANDE 733 { size: 'GRANDE', color: 'NERO', species: ['cornacchia', 'gallinella-d-acqua', 'folaga', 'taccola', 'gazza'] }, 734 { size: 'GRANDE', color: 'BIANCO', species: ['svasso-maggiore', 'gabbiano-comune', 'gabbiano-comune-inverno'] }, 735 { size: 'GRANDE', color: 'BIANCO/NERO', species: ['folaga', 'gazza', 'cavaliere-ditalia'] }, 736 { size: 'GRANDE', color: 'NERO/GRIGIO', species: ['cornacchia', 'taccola'] }, 737 { size: 'GRANDE', color: 'BEIGE', species: ['sparviere', 'ghiandaia', 'gheppio', 'tarabusino', 'mestolone-maschio', 'mestolone-femmina', 'lodolaio', 'allocco', 'gufo-comune', 'tortora-selvatica', 'germano-reale'] }, 738 { size: 'GRANDE', color: 'MARRONE', species: ['sparviere', 'ghiandaia', 'gheppio', 'tarabusino', 'mestolone-maschio', 'mestolone-femmina', 'lodolaio', 'allocco', 'gufo-comune', 'tortora-selvatica', 'germano-reale'] }, 739 { size: 'GRANDE', color: 'MARRONE/ROSSO', species: ['sparviere', 'ghiandaia', 'gheppio', 'tarabusino', 'lodolaio', 'allocco', 'gufo-comune', 'tortora-selvatica'] }, 740 { size: 'GRANDE', color: 'GRIGIO', species: ['piccione', 'colombaccio', 'colombella', 'falco-pellegrino', 'tortora-dal-collare', 'gabbiano-comune', 'gabbiano-comune-inverno', 'cuculo', 'tortora-selvatica'] }, 741 { size: 'GRANDE', color: 'VERDE', species: ['parrocchetto-collare', 'parrocchetto-monaco', 'picchio-verde', 'germano-reale'] }, 742 { size: 'GRANDE', color: 'BLU', species: ['gazza'] }, 743 744 // MOLTO GRANDE 745 { size: 'MOLTO GRANDE', color: 'NERO', species: ['cormorano'] }, 746 { size: 'MOLTO GRANDE', color: 'BIANCO', species: ['airone-guardabuoi', 'garzetta', 'ibis-sacro', 'cicogna', 'svasso-maggiore', 'gabbiano-reale', 'cigno-reale', 'smergo-maggiore-maschio'] }, 747 { size: 'MOLTO GRANDE', color: 'BIANCO/NERO', species: ['ibis-sacro', 'cicogna'] }, 748 { size: 'MOLTO GRANDE', color: 'NERO/GRIGIO', species: ['airone-cenerino'] }, 749 { size: 'MOLTO GRANDE', color: 'GRIGIO', species: ['airone-cenerino', 'nitticora', 'gabbiano-reale', 'airone-rosso'] }, 750 { size: 'MOLTO GRANDE', color: 'BEIGE', species: ['fagiano-femmina', 'svasso-maggiore', 'poiana'] }, 751 { size: 'MOLTO GRANDE', color: 'MARRONE', species: ['fagiano-maschio', 'svasso-maggiore', 'poiana', 'smergo-maggiore-femmina'] }, 752 { size: 'MOLTO GRANDE', color: 'ROSSO', species: ['fagiano-maschio', 'airone-rosso'] }, 753 { size: 'MOLTO GRANDE', color: 'VERDE', species: ['smergo-maggiore-maschio'] }, 754 { size: 'MOLTO GRANDE', color: 'GRIGIO/ARANCIO', species: ['smergo-maggiore-femmina'] }, 755 ]; 756 757 function updateBlocks(){ 758 const val = (document.querySelector('input[name="conosciSpecieLocal"]:checked')||{}).value; 759 const sizeVal = (document.querySelector('input[name="dimensione"]:checked')||{}).value; 760 761 if (species) species.style.display = (val === 'si') ? 'block' : 'none'; 762 763 if (val === 'no') { 764 if(dimensionePanel) dimensionePanel.style.display = 'block'; 765 if (sizeVal) { 766 if(colorPanel) colorPanel.style.display = 'block'; 767 updateSizeImage(sizeVal); 768 filterColors(sizeVal); 769 770 // Aggiorna schede specie se c'è un colore selezionato 771 const colorVal = (document.querySelector('.color-btn.selected') || {}).getAttribute('data-color'); 772 if (colorVal) { 773 updateSpeciesCards(sizeVal, colorVal); 774 } 775 } else { 776 if(colorPanel) colorPanel.style.display = 'none'; 777 if(sizeRefImage) sizeRefImage.style.display = 'none'; 778 clearSpeciesCards(); 779 } 780 } else { 781 if(colorPanel) colorPanel.style.display = 'none'; 782 if(dimensionePanel) dimensionePanel.style.display = 'none'; 783 if(sizeRefImage) sizeRefImage.style.display = 'none'; 784 clearSpeciesCards(); 785 } 786 } 787 788 function clearSpeciesCards() { 789 const container = document.getElementById('speciesCardsContainer'); 790 if(container) { 791 container.innerHTML = ''; 792 container.style.display = 'none'; 793 } 794 const hiddenColor = document.getElementById('colorePredominante'); 795 if(hiddenColor) hiddenColor.value = ''; 796 document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('selected')); 797 } 798 799 function resolveSpeciesImagePath(src) { 800 const s = String(src || '').trim(); 801 if (!s) return s; 802 if (/^(https?:)?\/\//i.test(s) || s.startsWith('data:') || s.startsWith('blob:')) return s; 803 if (s.startsWith('/assets/species/')) return s; 804 const file = s.split('/').pop(); 805 const remap = { 806 'Carcharodus alceae ali aperte.jpg': 'Carcharodus alceae ali aperte__uccelli.jpg',
807 'Gonepteryx rhamni_ali chiuse.jpg': 'Gonepteryx rhamni_ali chiuse__uccelli.jpg', 808 'airone rosso.jpg': 'airone rosso__uccelli.jpg', 809 'capriolo.jpg': 'capriolo__uccelli.jpg', 810 'picchio rosso maggiore.jpg': 'picchio rosso maggiore__uccelli.jpg', 811 'raganella.jpg': 'raganella__uccelli.jpg', 812 'rondine comune.jpg': 'rondine comune__uccelli.jpg', 813 }; 814 const finalName = remap[file] || file; 815 return '/assets/species/' + encodeURIComponent(finalName); 816 } 817 818 function resolveSpeciesCardImagePath(species, colorVal) { 819 if (!species) return ''; 820 if (species.id === 'germano-reale') { 821 const c = String(colorVal || '').trim().toUpperCase(); 822 if (c === 'BEIGE' || c === 'MARRONE') { 823 return 'https://commons.wikimedia.org/wiki/Special:FilePath/Female%20Mallard%20Duck%20Rest%203.jpg?width=1200'; 824 } 825 } 826 return resolveSpeciesImagePath(species.image); 827 } 828 829 function updateSpeciesCards(sizeVal, colorVal) { 830 const container = document.getElementById('speciesCardsContainer'); 831 if(!container) return; 832 833 container.innerHTML = ''; 834 835 if(!sizeVal || !colorVal) { 836 container.style.display = 'none'; 837 return; 838 } 839 840 // Trova mapping 841 let sizeKey = ''; 842 if(sizeVal.includes('PICCOLO')) sizeKey = 'PICCOLO'; 843 else if(sizeVal.includes('MEDIO')) sizeKey = 'MEDIO'; 844 else if(sizeVal.includes('GRANDE') && !sizeVal.includes('MOLTO')) sizeKey = 'GRANDE'; 845 else if(sizeVal.includes('MOLTO GRANDE')) sizeKey = 'MOLTO GRANDE'; 846 847 const mapping = speciesMappings.find(m => m.size === sizeKey && m.color === colorVal); 848 849 if(!mapping || !mapping.species || mapping.species.length === 0) { 850 container.innerHTML = '<p style="grid-column:1/-1; text-align:center; color:#666;">Nessuna specie trovata con queste caratteristiche.</p>'; 851 container.style.display = 'grid'; // Grid layout defined in CSS 852 return; 853 } 854 855 // Filtra database 856 let speciesToShow = speciesDatabase.filter(s => mapping.species.includes(s.id)); 857 858 // Ordina alfabeticamente per nome 859 speciesToShow.sort((a, b) => a.name.localeCompare(b.name)); 860 861 if(speciesToShow.length === 0) { 862 container.innerHTML = '<p style="grid-column:1/-1; text-align:center; color:#666;">Nessuna specie trovata nel database.</p>'; 863 container.style.display = 'grid'; 864 return; 865 } 866 867 // Render cards 868 speciesToShow.forEach(s => { 869 const card = document.createElement('div'); 870 const imgSrc = resolveSpeciesCardImagePath(s, colorVal); 871 card.className = 'species-card'; 872 // Click sulla card seleziona la specie 873 card.onclick = (e) => {
874 // Se il click non è avvenuto sull'immagine o sul pulsante, seleziona la specie 875 if(!e.target.closest('.species-card-img-container') && !e.target.closest('.species-select-btn')) { 876 selectSpeciesFromCard(s); 877 } 878 }; 879 880 // Helper for phenology 881 const generatePhenology = (species) => { 882 // SE SPECIE E' GABBIANO COMUNE -> NIENTE FENOLOGIA 883 if(species.id === 'gabbiano-comune' || species.id === 'gabbiano-comune-inverno') return ''; 884 885 const months = species.months || []; 886 const nesting = species.nesting || []; 887 const phenology = species.phenology || {}; 888 889 let html = '<div class="phenology-container">'; 890 const headers = ['G','F','M','A','M','G','L','A','S','O','N','D']; 891 892 for(let i=0; i<12; i++) { 893 let decades = ['phen-white', 'phen-white', 'phen-white']; 894 895 if (phenology[i]) { 896 if (Array.isArray(phenology[i]) && phenology[i].length === 3) { 897 decades = phenology[i]; 898 } else { 899 // Legacy string mapping 900 const cls = phenology[i]; 901 if (cls === 'phen-green') decades = ['phen-green', 'phen-green', 'phen-green']; 902 else if (cls === 'phen-yellow') decades = ['phen-yellow', 'phen-yellow', 'phen-yellow']; 903 else if (cls === 'phen-white') decades = ['phen-white', 'phen-white', 'phen-white']; 904 905 // Complex gradients (approximate to decades) 906 else if (cls === 'phen-green-yellow') decades = ['phen-green', 'phen-green-yellow', 'phen-yellow']; 907 else if (cls === 'phen-green-yellow-25') decades = ['phen-green', 'phen-yellow', 'phen-yellow']; 908 else if (cls === 'phen-green-yellow-75') decades = ['phen-green', 'phen-green', 'phen-yellow']; 909 910 else if (cls === 'phen-yellow-green') decades = ['phen-yellow', 'phen-yellow-green', 'phen-green']; 911 else if (cls === 'phen-yellow-green-25') decades = ['phen-yellow', 'phen-green', 'phen-green']; 912 else if (cls === 'phen-yellow-green-75') decades = ['phen-yellow', 'phen-yellow', 'phen-green']; 913 914 else if (cls === 'phen-white-green') decades = ['phen-white', 'phen-white-green', 'phen-green']; 915 else if (cls === 'phen-white-green-25') decades = ['phen-white', 'phen-green', 'phen-green']; 916 else if (cls === 'phen-white-green-75') decades = ['phen-white', 'phen-white', 'phen-green']; 917 918 else if (cls === 'phen-green-white') decades = ['phen-green', 'phen-green-white', 'phen-white']; 919 else if (cls === 'phen-green-white-25') decades = ['phen-green', 'phen-white', 'phen-white']; 920 else if (cls === 'phen-green-white-75') decades = ['phen-green', 'phen-green', 'phen-white']; 921 922 else if (cls === 'phen-mix-aug-pigliamosche') decades = ['phen-yellow', 'phen-white', 'phen-green']; 923 924 else decades = [cls, cls, cls]; 925 } 926 } else { 927 let base = 'phen-white'; 928 if(nesting.includes(i)) base = 'phen-yellow'; 929 else if(months.includes(i)) base = 'phen-green'; 930 decades = [base, base, base]; 931 } 932 933 // Render Month Box 934 html += `<div class="phen-month-box">`; 935 // Decades 936 decades.forEach(d => { 937 html += `<div class="phen-decade ${d}"></div>`; 938 }); 939 // Letter Overlay 940 html += `<span class="phen-letter">${headers[i]}</span>`;
941 html += `</div>`; 942 } 943 944 html += '</div>'; 945 946 // Legenda 947 html += '<div style="margin-top:4px; font-size:11px; color:#555; display:flex; align-items:center; gap:12px; padding-left: 2%;">'; 948 html += '<div style="display:flex; align-items:center;"><span style="width:10px; height:10px; background:#8cc63f; display:inline-block; margin-right:4px; border-radius:2px;"></span>Presenza</div>'; 949 html += '<div style="display:flex; align-items:center;"><span style="width:10px; height:10px; background:#fff200; display:inline-block; margin-right:4px; border-radius:2px;"></span>Nidificazione</div>'; 950 html += '</div>'; 951 952 return html; 953 }; 954 955 card.innerHTML = ` 956 <div class="species-card-img-container" style="position: relative; overflow: hidden; background-color: #f1f5f9;"> 957 <div class="blur-bg" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; 958 background-image: url('${imgSrc}'); background-size: cover; background-position: center; 959 filter: blur(10px) brightness(0.8); transform: scale(1.1); opacity: 0.8; z-index: 0;"></div> 960 <img src="${imgSrc}" alt="${s.name}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; z-index: 1;" onclick="event.stopPropagation(); openSpeciesLightbox('${imgSrc}')"> 961 </div> 962 ${generatePhenology(s)} 963 <div class="species-card-header"> 964 <span class="species-common-name">${s.name}</span> 965 <span class="species-sci-name">(${s.scientificName})</span> 966 </div> 967 <div class="species-card-body"> 968 <div class="species-recognition"> 969 <strong>Come riconoscerlo:</strong> ${s.recognition || 'Descrizione non disponibile.'} 970 </div> 971 <div class="species-habitat"> 972 <strong>Habitat:</strong> ${s.habitat || 'Informazioni sull\'habitat non disponibili.'} 973 </div> 974 <button type="button" class="btn species-select-btn"> 975 Seleziona specie 976 </button> 977 </div> 978 `; 979 980 // Attach event listener to button 981 const btn = card.querySelector('.species-select-btn'); 982 if(btn) { 983 btn.addEventListener('click', (e) => { 984 e.stopPropagation(); 985 selectSpeciesFromCard(s); 986 }); 987 } 988 989 container.appendChild(card); 990 }); 991 992 container.style.display = 'grid'; 993 } 994 995 function selectSpeciesFromCard(species) { 996 // Popola il campo nomeSpecie 997 const nomeSpecieInput = document.getElementById('nomeSpecie'); 998 if(nomeSpecieInput) { 999 const fullName = species.scientificName ? `${species.name} (${species.scientificName})` : species.name; 1000 nomeSpecieInput.value = fullName; 1001 1002 // Trigger Rondoni check 1003 checkRondoni(nomeSpecieInput); 1004 1005 // Imposta 'Conosci specie' a 'Si' per mostrare i campi 1006 const conosciSi = document.querySelector('input[name="conosciSpecieLocal"][value="si"]'); 1007 if(conosciSi) { 1008 conosciSi.checked = true; 1009 conosciSi.dispatchEvent(new Event('change')); 1010 } 1011 1012 // Scorri verso i campi specie 1013 document.getElementById('speciesFields').scrollIntoView({behavior: 'smooth'}); 1014 } 1015 } 1016 1017 function checkRondoni(input) { 1018 const val = input.value.toLowerCase(); 1019 const cavitaRow = document.querySelector('.cavita-row'); 1020 if(cavitaRow) { 1021 if(val.includes('rondone')) { 1022 cavitaRow.style.display = 'block'; 1023 } else { 1024 cavitaRow.style.display = 'none'; 1025 } 1026 } 1027 } 1028 1029 function updateSizeImage(sizeVal) { 1030 if(!sizeRefImage) return; 1031 1032 let imgSrc = ''; 1033 if(sizeVal.includes('PICCOLO')) imgSrc = 'grafica/mano_pettirosso.png'; 1034 else if(sizeVal.includes('MEDIO')) imgSrc = 'grafica/mano_media_merlo.png'; 1035 else if(sizeVal.includes('GRANDE') && !sizeVal.includes('MOLTO')) imgSrc = 'grafica/mano_grande_cornacchia.png'; 1036 else if(sizeVal.includes('MOLTO GRANDE')) imgSrc = 'grafica/molto_grande_airone_cenerino.png'; 1037 1038 if(imgSrc) { 1039 const img = sizeRefImage.querySelector('img'); 1040 if(img) img.src = imgSrc; 1041 sizeRefImage.style.display = 'block'; 1042 } 1043 } 1044 1045 function filterColors(sizeVal) { 1046 // Reset all colors 1047 colorBtns.forEach(btn => btn.style.display = 'flex'); 1048 1049 // In base alla dimensione, mostra solo i colori disponibili 1050 // Usiamo mappings per determinare quali colori sono validi per la size 1051 let sizeKey = ''; 1052 if(sizeVal.includes('PICCOLO')) sizeKey = 'PICCOLO'; 1053 else if(sizeVal.includes('MEDIO')) sizeKey = 'MEDIO'; 1054 else if(sizeVal.includes('GRANDE') && !sizeVal.includes('MOLTO')) sizeKey = 'GRANDE'; 1055 else if(sizeVal.includes('MOLTO GRANDE')) sizeKey = 'MOLTO GRANDE'; 1056 1057 // Ottieni colori validi 1058 const validColors = speciesMappings 1059 .filter(m => m.size === sizeKey) 1060 .map(m => m.color); 1061 1062 // Nascondi colori non validi 1063 colorBtns.forEach(btn => { 1064 const color = btn.getAttribute('data-color'); 1065 // Rimuovi specifici colori se size è MEDIO 1066 if(sizeKey === 'MEDIO' && (color === 'NERO/GRIGIO' || color === 'GIALLO/BLU' || color === 'ROSSO/GIALLO')) { 1067 btn.style.display = 'none'; 1068 return; 1069 } 1070 if(!validColors.includes(color)) { 1071 btn.style.display = 'none'; 1072 } 1073 }); 1074 } 1075 1076 // Event Listeners 1077 radios.forEach(r => r.addEventListener('change', updateBlocks)); 1078 dimRadios.forEach(r => r.addEventListener('change', updateBlocks)); 1079 1080 // Color Buttons 1081 if(colorPanel) { 1082 colorPanel.addEventListener('click', (e) => { 1083 const btn = e.target.closest('.color-btn'); 1084 if(!btn) return; 1085 1086 // Toggle selection 1087 document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('selected')); 1088 btn.classList.add('selected'); 1089 1090 const hidden = document.getElementById('colorePredominante'); 1091 if(hidden) hidden.value = btn.getAttribute('data-color'); 1092 1093 // Update cards 1094 const sizeVal = (document.querySelector('input[name="dimensione"]:checked')||{}).value; 1095 updateSpeciesCards(sizeVal, btn.getAttribute('data-color')); 1096 }); 1097 } 1098 1099 // Input nomeSpecie change 1100 const nomeSpecieInput = document.getElementById('nomeSpecie'); 1101 if(nomeSpecieInput) { 1102 nomeSpecieInput.addEventListener('input', () => checkRondoni(nomeSpecieInput)); 1103 } 1104 1105 // Cavità 1106 const cavitaRadios = document.querySelectorAll('input[name="q78_haiOsservato[]"]'); 1107 cavitaRadios.forEach(r => { 1108 r.addEventListener('change', () => { 1109 const details = document.querySelector('.cavita-detail
1109s-row'); 1110 if(details) { 1111 details.style.display = (r.value === 'Si') ? 'block' : 'none'; 1112 } 1113 }); 1114 }); 1115 1116 // ========= FILE UPLOAD & PREVIEW ========= 1117 const fileInput = document.getElementById('birdImage'); 1118 const previewContainer = document.getElementById('previewContainer'); 1119 const imagePreviewBox = document.getElementById('imagePreviewBox'); 1120 1121 if(fileInput) { 1122 fileInput.addEventListener('change', handleFileSelect); 1123 } 1124 1125 // NUOVA FUNZIONE: Converte il file HEIC/HEIF in Blob JPEG 1126 async function convertFile(file) { 1127 if ( 1128 file.type === "image/heic" || 1129 file.type === "image/heif" || 1130 (file.name && file.name.toLowerCase().endsWith(".heic")) || 1131 (file.name && file.name.toLowerCase().endsWith(".heif")) 1132 ) { 1133 console.log(`Tentativo di conversione del file HEIC/HEIF: ${file.name}`); 1134 if (typeof heic2any === "undefined") { 1135 console.error("Libreria heic2any non caricata."); 1136 alert("Errore: la libreria di conversione HEIC non è disponibile."); 1137 return null; 1138 } 1139 try { 1140 const jpegBlob = await heic2any({ 1141 blob: file, 1142 toType: "image/jpeg", 1143 quality: 0.9, 1144 }); 1145 const convertedBlob = Array.isArray(jpegBlob) ? jpegBlob[0] : jpegBlob; 1146 const newFileName = (file.name || "immagine").replace(/\.heic|\.heif/i, "") + ".jpg"; 1147 return new File([convertedBlob], newFileName, { 1148 type: "image/jpeg", 1149 lastModified: file.lastModified, 1150 }); 1151 } catch (e) { 1152 console.error("Errore durante la conversione HEIC:", e); 1153 alert(`Impossibile convertire il file ${file.name} (formato HEIC/HEIF). Caricare una foto JPG/PNG.`); 1154 return null; 1155 } 1156 } 1157 return file; 1158 } 1159 1160 async function compressImageToTarget(file, targetBytes) { 1161 try { 1162 if (!file || !file.type || !file.type.startsWith("image/")) return file; 1163 if (file._guardamiProcessed === "1") return file; 1164 const target = Number(targetBytes) || 0; 1165 if (target > 0 && file.size && file.size <= target) return file; 1166 1167 const img = await new Promise((resolve, reject) => { 1168 const url = URL.createObjectURL(file); 1169 const i = new Image(); 1170 i.onload = () => { 1171 URL.revokeObjectURL(url); 1172 resolve(i); 1173 }; 1174 i.onerror = () => { 1175 URL.revokeObjectURL(url); 1176 reject(new Error("img-load")); 1177 }; 1178 i.src = url; 1179 }); 1180 1181 const w0 = img.naturalWidth || img.width; 1182 const h0 = img.naturalHeight || img.height; 1183 if (!w0 || !h0) return file; 1184 1185 const maxSides = [1600, 1280, 1024]; 1186 const qualities = [0.82, 0.72, 0.62, 0.52]; 1187 1188 for (const maxSide of maxSides) { 1189 const scale = Math.min(1, maxSide / Math.max(w0, h0)); 1190 const tw = Math.max(1, Math.round(w0 * scale)); 1191 const th = Math.max(1, Math.round(h0 * scale)); 1192 const canvas = document.createElement("canvas"); 1193 canvas.width = tw; 1194 canvas.height = th; 1195 const ctx = canvas.getContext("2d"); 1196 if (!ctx) break; 1197 ctx.drawImage(img, 0, 0, tw, th); 1198 1199 for (const q of qualities) { 1200 const blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b), "image/jpeg", q)); 1201 if (!blob) continue; 1202 const newName = (file.name || "immagine").replace(/\.(jpe?g|png|webp|heic|heif)$/i, "") + ".jpg"; 1203 const out = new File([blob], newName, { type: "image/jpeg" }); 1204 out._guardamiProcessed = "1"; 1205 if (target > 0 && out.size <= target) return out; 1206 if (target <= 0 && out.size < file.size) return out; 1207 if (target > 0 && out.size <= Math.max(target * 1.05, target + 40 * 1024)) return out; 1208 } 1209 } 1210 1211 return file; 1212 } catch (e) { 1213 return file; 1214 } 1215 } 1216 1217 async function handleFileSelect(e) { 1218 const rawFiles = Array.from(e.target.files); 1219 if(rawFiles.length === 0) return; 1220 1221 // Reset input value to allow re-selecting same file 1222 e.target.value = ''; 1223 1224 for(const rawFile of rawFiles) { 1225 if(selectedFiles.length >= MAX_FILES) { 1226 alert(`Puoi caricare massimo ${MAX_FILES} foto.`); 1227 break; 1228 } 1229 1230 // CONVERSIONE HEIC 1231 let file = await convertFile(rawFile); 1232 if(!file) continue; 1233 const currentTotalBefore = selectedFiles.reduce((acc, item) => acc + item.file.size, 0); 1234 const remaining = Math.max(0, MAX_TOTAL_BYTES - currentTotalBefore); 1235 const target = remaining > 0 ? remaining : MAX_TOTAL_BYTES; 1236 file = await compressImageToTarget(file, target); 1237 1238 // Check Total Size 1239 const currentTotal = selectedFiles.reduce((acc, item) => acc + item.file.size, 0); 1240 if(currentTotal + file.size > MAX_TOTAL_BYTES) { 1241 const maxMb = (MAX_TOTAL_BYTES / (1024 * 1024)).toFixed(0); 1242 alert(`Dimensione totale file superata (Max ${maxMb}MB).`); 1243 break; 1244 } 1245 1246 selectedFiles.push({ 1247 id: Date.now() + Math.random().toString(), 1248 file: file 1249 }); 1250 1251 // --- EXIF CHECK --- 1252 try { 1253 if (window.exifr) { 1254 const toDecimal = (dms, ref) => { 1255 if (!Array.isArray(dms) || dms.length < 3) return null; 1256 const toNum = (v) => { 1257 if (typeof v === "number") return v; 1258 if (v && typeof v === "object") { 1259 if (typeof v.numerator === "number" && typeof v.denominator === "number" && v.denominator) return v.numerator / v.denominator; 1260 if (typeof v.num === "number" && typeof v.den === "number" && v.den) return v.num / v.den; 1261 if (typeof v.valueOf === "function") {
1262 const n = Number(v.valueOf()); 1263 if (Number.isFinite(n)) return n; 1264 } 1265 } 1266 const n = Number(v); 1267 return Number.isFinite(n) ? n : null; 1268 }; 1269 const deg = toNum(dms[0]); 1270 const min = toNum(dms[1]); 1271 const sec = toNum(dms[2]); 1272 if (!Number.isFinite(deg) || !Number.isFinite(min) || !Number.isFinite(sec)) return null; 1273 let dec = deg + min / 60 + sec / 3600; 1274 const r = String(ref || "").toUpperCase(); 1275 if (r === "S" || r === "W") dec = -dec; 1276 return dec; 1277 }; 1278 1279 const extractCoords = (meta) => { 1280 if (!meta || typeof meta !== "object") return null; 1281 if (typeof meta.latitude === "number" && typeof meta.longitude === "number") return { lat: meta.latitude, lng: meta.longitude }; 1282 const lat = toDecimal(meta.GPSLatitude, meta.GPSLatitudeRef); 1283 const lng = toDecimal(meta.GPSLongitude, meta.GPSLongitudeRef); 1284 if (typeof lat === "number" && typeof lng === "number") return { lat, lng }; 1285 if (typeof meta.gpsLatitude === "number" && typeof meta.gpsLongitude === "number") return { lat: meta.gpsLatitude, lng: meta.gpsLongitude }; 1286 return null; 1287 }; 1288 1289 const pickDate = (meta) => 1290 (meta && (meta.DateTimeOriginal || meta.CreateDate || meta.ModifyDate || meta.DateTimeDigitized || meta.dateTimeOriginal || meta.createDate || meta.modifyDate)) || null; 1291 const parseExifDate = (value) => { 1292 if (!value) return null; 1293 if (value instanceof Date) return isNaN(value.getTime()) ? null : value; 1294 const d = new Date(value); 1295 return isNaN(d.getTime()) ? null : d; 1296 }; 1297 1298 const output = await exifr.parse(file).catch(() => null); 1299 if (output) { 1300 const dateVal = pickDate(output); 1301 const d = parseExifDate(dateVal); 1302 if (d) { 1303 const yyyy = d.getFullYear(); 1304 const mm = String(d.getMonth() + 1).padStart(2, '0'); 1305 const dd = String(d.getDate()).padStart(2, '0'); 1306 const dateField = document.getElementById('dataAvvistamento'); 1307 if (dateField) dateField.value = `${yyyy}-${mm}-${dd}`; 1308 } 1309 1310 let coords = extractCoords(output); 1311 if (!coords && typeof exifr.gps === "function") { 1312 try { 1313 const gps = await exifr.gps(file); 1314 if (gps && typeof gps.latitude === "number" && typeof gps.longitude === "number") coords = { lat: gps.latitude, lng: gps.longitude }; 1315 } catch (e) {} 1316 } 1317 1318 if (coords) { 1319 const freezeKey = "__guardami_freeze_geo_uccelli_citizen_v1"; 1320 if (window[freezeKey]) return; 1321 const pos = { lat: coords.lat, lng: coords.lng }; 1322 window.__guardamiPendingExifCoords = pos; 1323 window.manualEditMade = true; 1324 const latInput = document.getElementById('latitudine'); 1325 const lngInput = document.getElementById('longitudine'); 1326 if (latInput && lngInput) { 1327 latInput.value = coords.lat.toFixed(6); 1328 lngInput.value = coords.lng.toFixed(6); 1329 } 1330 syncGeoToJotform(); 1331 const w = document.getElementById('exifWarningBox'); 1332 if(w) w.style.display = 'none'; 1333 if (window.map && window.marker) { 1334 window.map.setCenter(pos); 1335 window.map.setZoom(15); 1336 window.marker.setPosition(pos); 1337 window.__guardamiPendingExifCoords = null; 1338 } 1339 } else { 1340 const latInput = document.getElementById('latitudine'); 1341 if (!window.manualEditMade && latInput && !latInput.value) { 1342 const w = document.getElementById('exifWarningBox'); 1343 if(w) w.style.display = 'block'; 1344 } 1345 } 1346 } 1347 } 1348 } catch(e) { 1349 console.warn("EXIF error", e); 1350 } 1351 } 1352 1353 renderPreview(); 1354 } 1355 1356 function syncInputFiles() { 1357 if(!fileInput) return; 1358 const dt = new DataTransfer();
1359 selectedFiles.forEach(item => { 1360 // item.file is the File object 1361 dt.items.add(item.file); 1362 }); 1363 fileInput.files = dt.files; 1364 } 1365 1366 function renderPreview() { 1367 if(!previewContainer) return; 1368 previewContainer.innerHTML = ''; 1369 1370 syncInputFiles(); // Sync input with selectedFiles 1371 1372 if(selectedFiles.length > 0) { 1373 imagePreviewBox.style.display = 'block'; 1374 } else { 1375 imagePreviewBox.style.display = 'none'; 1376 } 1377 1378 selectedFiles.forEach((item, index) => { 1379 const div = document.createElement('div'); 1380 div.className = 'preview-item'; 1381 1382 const img = document.createElement('img'); 1383 const reader = new FileReader(); 1384 reader.onload = (e) => img.src = e.target.result; 1385 reader.readAsDataURL(item.file); 1386 1387 if(item.file.type.startsWith('image/')) { 1388 img.style.cursor = 'pointer'; 1389 img.onclick = () => window.openLightbox(index); 1390 } 1391 1392 const delBtn = document.createElement('button'); 1393 delBtn.className = 'thumbnail-delete'; 1394 delBtn.textContent = 'Ã'; 1395 delBtn.onclick = () => removeFile(index); 1396 1397 div.appendChild(img); 1398 div.appendChild(delBtn); 1399 previewContainer.appendChild(div); 1400 }); 1401 } 1402 1403 window.removeFile = function(index) { 1404 selectedFiles.splice(index, 1); 1405 renderPreview(); 1406 1407 // Se non ci sono più file, resetta data e posizione 1408 if (selectedFiles.length === 0) { 1409 // Reset Data to Today 1410 const dateField = document.getElementById('dataAvvistamento'); 1411 if (dateField) { 1412 const today = new Date(); 1413 const yyyy = today.getFullYear(); 1414 const mm = String(today.getMonth() + 1).padStart(2, '0'); 1415 const dd = String(today.getDate()).padStart(2, '0'); 1416 dateField.value = `${yyyy}-${mm}-${dd}`; 1417 } 1418 1419 // Reset Geolocation 1420 if (window.chiediGeolocalizzazione) { 1421 window.manualEditMade = false; // Reset manual edit flag 1422 window.chiediGeolocalizzazione(); 1423 } 1424 1425 // Reset EXIF warning 1426 const w = document.getElementById('exifWarningBox'); 1427 if(w) w.style.display = 'none'; 1428 } 1429 } 1430 1431 1432 function readFileAsDataURL(file) { 1433 return new Promise((resolve, reject) => { 1434 const reader = new FileReader(); 1435 reader.onload = (e) => resolve(e.target.result); 1436 reader.onerror = (e) => reject(e); 1437 reader.readAsDataURL(file); 1438 }); 1439 } 1440 1441 1442 // ========= SUBMISSION ========= 1443 const form = document.getElementById('bird-form'); 1444 const submitBtn = document.getElementById('submitButton'); // Assicurarsi che esista nell'HTML o aggiungerlo se manca (Jotform usa button class="form-submit-button") 1445 1446 // Se non c'è ID, cerchiamo il bottone submit generico 1447 const realSubmitBtn = submitBtn || form.querySelector('button[type="submit"]'); 1448 1449 if(form) { 1450 form.noValidate = true; // Disabilita validazione browser per gestire tutto via JS 1451 1452 form.addEventListener('submit', async (e) => { 1453 e.preventDefault(); // Blocca sempre l'invio standard per usare la nostra logica 1454 1455 const freezeKey = "__guardami_freeze_geo_uccelli_citizen_v1"; 1456 window[freezeKey] = true; 1457 let hasCoords = false; 1458 try { 1459 const latInput = document.getElementById('latitudine'); 1460 const lngInput = document.getElementById('longitudine'); 1461 const latV = latInput ? parseFloat(latInput.value) : NaN; 1462 const lngV = lngInput ? parseFloat(lngInput.value) : NaN; 1463 if (Number.isFinite(latV) && Number.isFinite(lngV)) hasCoords = true; 1464 } catch (e) {} 1465 try { 1466 if (!hasCoords && window.marker && typeof window.marker.getPosition === 'function') { 1467 const pos = window.marker.getPosition(); 1468 if (pos && typeof pos.lat === 'function' && typeof pos.lng === 'function') { 1469 const latV = pos.lat(); 1470 const lngV = pos.lng(); 1471 if (Number.isFinite(latV) && Number.isFinite(lngV)) hasCoords = true; 1472 } 1473 } 1474 } catch (e) {} 1475 const tryFreezeGpsNow = async () => { 1476 try { 1477 if (hasCoords) return true; 1478 if (window.manualEditMade) return true; 1479 if (!navigator.geolocation) return false;
1480 const ok = await new Promise((resolve) => { 1481 navigator.geolocation.getCurrentPosition( 1482 (pos) => { 1483 try { 1484 const la = pos && pos.coords ? pos.coords.latitude : null; 1485 const lo = pos && pos.coords ? pos.coords.longitude : null; 1486 if (typeof la !== "number" || typeof lo !== "number" || !isFinite(la) || !isFinite(lo)) return resolve(false); 1487 const p = { lat: la, lng: lo }; 1488 if (window.map && window.marker) { 1489 window.map.setCenter(p); 1490 window.map.setZoom(15); 1491 window.marker.setPosition(p); 1492 } 1493 const latInput = document.getElementById('latitudine'); 1494 const lngInput = document.getElementById('longitudine'); 1495 if (latInput) latInput.value = la.toFixed(6); 1496 if (lngInput) lngInput.value = lo.toFixed(6); 1497 try { if (typeof syncGeoToJotform === "function") syncGeoToJotform(); } catch (e) {} 1498 resolve(true); 1499 } catch (e) { 1500 resolve(false); 1501 } 1502 }, 1503 () => resolve(false), 1504 { enableHighAccuracy: true, maximumAge: 0, timeout: 8000 } 1505 ); 1506 }); 1507 return ok; 1508 } catch (e) { 1509 return false; 1510 } 1511 }; 1512 1513 await tryFreezeGpsNow(); 1514 1515 // === 0. CONTROLLO PERIMETRO (CITTà METROPOLITANA DI MILANO) === 1516 const latInput = document.getElementById('latitudine'); 1517 const lngInput = document.getElementById('longitudine'); 1518 let lat = latInput ? parseFloat(latInput.value) : NaN; 1519 let lng = lngInput ? parseFloat(lngInput.value) : NaN; 1520 1521 // Fallback se input non trovati o vuoti 1522 if (isNaN(lat) || isNaN(lng)) { 1523 const jg = document.getElementById('jotformGeoloc')?.value; 1524 if (jg && jg.includes(',')) { 1525 const parts = jg.split(','); 1526 lat = parseFloat(parts[0]); 1527 lng = parseFloat(parts[1]); 1528 } 1529 } 1530 1531 const safeOriginalBtnText = (realSubmitBtn && typeof realSubmitBtn.textContent === "string" && realSubmitBtn.textContent.trim()) ? realSubmitBtn.textContent : "Invia"; 1532 if (realSubmitBtn && !realSubmitBtn.dataset.originalText) realSubmitBtn.dataset.originalText = safeOriginalBtnText; 1533 const restoreSubmitButton = () => { 1534 if (!realSubmitBtn) return; 1535 realSubmitBtn.disabled = false; 1536 realSubmitBtn.textContent = (realSubmitBtn.dataset && realSubmitBtn.dataset.originalText) ? realSubmitBtn.dataset.originalText : safeOriginalBtnText; 1537 }; 1538 1539 if (!Number.isFinite(lat) || !Number.isFinite(lng)) { 1540 alert("Posizione mancante: consenti l'accesso al GPS oppure imposta la posizione sulla mappa prima di inviare."); 1541 return; 1542 } 1543 1544 const minLat = 45.10, maxLat = 45.70; 1545 const minLng = 8.80, maxLng = 9.70; 1546 const inBoundingBox = (lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng); 1547 if (!inBoundingBox) { 1548 const modal = document.getElementById('outOfBoundsModal'); 1549 if (modal) { 1550 modal.style.display = 'flex'; 1551 const closeBtn = document.getElementById('closeOutOfBoundsBtn'); 1552 if (closeBtn) closeBtn.onclick = () => modal.style.display = 'none'; 1553 modal.onclick = (ev) => { 1554 if (ev.target === modal) modal.style.display = 'none'; 1555 }; 1556 } else { 1557 alert("â ï¸ Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo non può essere inviato."); 1558 } 1559 return; 1560 } 1561 1562 if (!window.google || !window.google.maps || !window.google.maps.Geocoder) { 1563 alert("Impossibile verificare la posizione (servizio mappe non disponibile). Riprova tra poco."); 1564 return; 1565 } 1566 1567 if (realSubmitBtn) { 1568 realSubmitBtn.disabled = true; 1569 realSubmitBtn.textContent = "Verifica posizione..."; 1570 } 1571 1572 let geoVerified = false;
1573 let isMilano = false; 1574 try { 1575 const geoResult = await new Promise((resolve) => { 1576 let done = false; 1577 const timeoutId = setTimeout(() => { 1578 done = true; 1579 resolve({ verified: false, inMilano: false }); 1580 }, 6000); 1581 1582 new google.maps.Geocoder().geocode({ location: { lat, lng } }, (results, status) => { 1583 if (done) return; 1584 clearTimeout(timeoutId); 1585 if (status === "OK" && results && results.length > 0) { 1586 const inMilano = results.some((r) => 1587 (r.address_components || []).some((c) => { 1588 const types = c && c.types ? c.types : []; 1589 const ln = (c && c.long_name ? String(c.long_name) : "").toLowerCase(); 1590 const sn = (c && c.short_name ? String(c.short_name) : "").toUpperCase(); 1591 return types.includes("administrative_area_level_2") && (ln.includes("milano") || sn === "MI"); 1592 }) 1593 ); 1594 resolve({ verified: true, inMilano }); 1595 return; 1596 } 1597 resolve({ verified: false, inMilano: false }); 1598 }); 1599 }); 1600 geoVerified = !!(geoResult && geoResult.verified); 1601 isMilano = !!(geoResult && geoResult.inMilano); 1602 } catch (e2) { 1603 geoVerified = false; 1604 isMilano = false; 1605 } 1606 1607 restoreSubmitButton(); 1608 1609 if (!geoVerified) { 1610 alert("Impossibile verificare la posizione in questo momento. Riprova tra poco."); 1611 return; 1612 } 1613 1614 if (!isMilano) { 1615 const modal = document.getElementById('outOfBoundsModal'); 1616 if (modal) { 1617 modal.style.display = 'flex'; 1618 const closeBtn = document.getElementById('closeOutOfBoundsBtn'); 1619 if (closeBtn) closeBtn.onclick = () => modal.style.display = 'none'; 1620 modal.onclick = (ev) => { 1621 if (ev.target === modal) modal.style.display = 'none'; 1622 }; 1623 } else { 1624 alert("â ï¸ Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo non può essere inviato."); 1625 } 1626 return; 1627 } 1628 1629 // === VALIDAZIONE COMPLETA === 1630 1631 // 1. Foto 1632 if(selectedFiles.length === 0) { 1633 alert("Carica almeno una foto."); 1634 return; 1635 } 1636 1637 // 2. Email 1638 const email = document.getElementById('email'); 1639 if(!email || !email.value || !email.value.includes('@')) { 1640 alert("Inserisci un'email valida."); 1641 return; 1642 } 1643 1644 // 3. Cosa hai osservato (Radio) 1645 const cosa = document.querySelector('input[name="q167_cosaHai"]:checked'); 1646 if(!cosa) { 1647 alert("Seleziona 'Cosa hai osservato' (Uccello vivo/morto)."); 1648 return; 1649 } 1650 1651 // 4. Conosci la specie (Radio) 1652 const known = document.querySelector('input[name="conosciSpecieLocal"]:checked'); 1653 if(!known) { 1654 alert("Indica se conosci la specie (Sì/No)."); 1655 return; 1656 } 1657 1658 const nomeSpecie = document.getElementById('nomeSpecie'); 1659 if(known.value === 'si' && (!nomeSpecie || !nomeSpecie.value.trim())) { 1660 alert("Hai indicato di conoscere la specie, per favore inserisci il nome."); 1661 return; 1662 } 1663 1664 // 5. Numero individui (Select) 1665 const numInd = document.getElementById('numeroIndividui'); 1666 if(!numInd || !numInd.value) { 1667 alert("Seleziona il numero di individui."); 1668 return; 1669 } 1670 1671 // 6. Sesso (Checkbox group) 1672 const sesso = document.querySelectorAll('input[name="q20_sesso[]"]:checked'); 1673 if(sesso.length === 0) { 1674 alert("Seleziona il sesso (o 'Non riconosciuto')."); 1675 return; 1676 } 1677 1678 if(realSubmitBtn) { 1679 realSubmitBtn.disabled = true; 1680 realSubmitBtn.textContent = 'Invio in corso...'; 1681 } 1682 1683 try { 1684 // --- PREPARAZIONE FILE (CONVERSIONE BASE64) --- 1685 const filesPayload = []; 1686 1687 // Riduzione lato client SOLO per immagini (per video/audio inviamo l'originale) 1688 async function compressImageIfNeeded(file) { 1689 try { 1690 if (!file || !file.type || !file.type.startsWith("image/")) return file; 1691 if (file._guardamiProcessed === "1") return file; 1692 if (file.size && file.size < 400 * 1024) return file; // <400KB ok 1693 1694 // Canvas compress: max 1600px lato lungo, JPEG quality 0.8 1695 const dataUrl = await readFileAsDataURL(file); 1696 const img = await new Promise((resolve, reject) => { 1697 const i = new Image(); 1698 i.onload = () => resolve(i); 1699 i.onerror = () => reject(new Error("Impossibile caricare l'immagine per la compressione.")); 1700 i.src = dataUrl; 1701 }); 1702 1703 const maxSide = 1600;
1704 let w = img.naturalWidth || img.width; 1705 let h = img.naturalHeight || img.height; 1706 if (!w || !h) return file; 1707 1708 const scale = Math.min(1, maxSide / Math.max(w, h)); 1709 const tw = Math.round(w * scale); 1710 const th = Math.round(h * scale); 1711 1712 const canvas = document.createElement("canvas"); 1713 canvas.width = tw; 1714 canvas.height = th; 1715 const ctx = canvas.getContext("2d"); 1716 if (!ctx) return file; 1717 ctx.drawImage(img, 0, 0, tw, th); 1718 1719 const blob = await new Promise((resolve) => { 1720 canvas.toBlob((b) => resolve(b), "image/jpeg", 0.8); 1721 }); 1722 if (!blob) return file; 1723 1724 const newName = (file.name || "immagine").replace(/\.(jpe?g|png|webp|heic|heif)$/i, "") + ".jpg"; 1725 return new File([blob], newName, { type: "image/jpeg" }); 1726 } catch (e) { 1727 console.warn("Compressione immagine fallita, invio originale:", e); 1728 return file; 1729 } 1730 } 1731 1732 for (const item of selectedFiles) { 1733 const f = item.file; 1734 1735 // Converte HEIC/HEIF in JPEG se necessario (funzione già presente nello script) 1736 let fileToSend = (f && f._guardamiProcessed === "1") ? f : await convertFile(f); 1737 if (!fileToSend) fileToSend = f; 1738 1739 // Comprimi solo se immagine 1740 if (fileToSend.type && fileToSend.type.startsWith("image/")) { 1741 fileToSend = await compressImageIfNeeded(fileToSend); 1742 } 1743 1744 // Lettura come Data URL 1745 const dataUrl = await readFileAsDataURL(fileToSend); 1746 const base64 = (dataUrl || "").toString().split(",")[1] || ""; 1747 1748 filesPayload.push({ 1749 fileBase64: base64, 1750 fileName: fileToSend.name || "file", 1751 fileType: fileToSend.type || "application/octet-stream" 1752 }); 1753 } 1754 1755 // --- RACCOLTA DATI FORM --- 1756 const sessoValues = Array.from(sesso).map(el => el.value); 1757 const ambienteValues = Array.from(document.querySelectorAll('input[name="q13_ambienteCircostante[]"]:checked')).map(el => el.value); 1758 1759 // Raccolta dati extra (Cavità , Atlante) 1760 const haiOsservatoCavita = (document.querySelector('input[name="q78_haiOsservato[]"]:checked') || {}).value || ""; 1761 const numeroCavita = document.getElementById('numeroCavita')?.value || ""; 1762 const numeroCivico = document.getElementById('numeroCivico')?.value || ""; 1763 const codiciAtlante = document.getElementById('codiciAtlanteInput')?.value || ""; // CSV string 1764 const noteAtlante = document.getElementById('altreOsservazioniAtlante')?.value || ""; 1765 1766 // Recupero UID (da querystring o da Firebase Auth) 1767 const urlParams = new URLSearchParams(window.location.search || ""); 1768 const uidFromQuery = urlParams.get('uid') || ""; 1769 const uidFromAuth = (window.firebaseAuth && window.firebaseAuth.currentUser) ? window.firebaseAuth.currentUser.uid : ""; 1770 const uid = uidFromQuery || uidFromAuth || ""; 1771 1772 const clientRequestKey = "guardami_pending_request_uccelli_cittadini"; 1773 let clientRequestId = ""; 1774 try { 1775 const raw = sessionStorage.getItem(clientRequestKey); 1776 if (raw) { 1777 const parsed = JSON.parse(raw); 1778 if (parsed && parsed.id && parsed.ts && (Date.now() - Number(parsed.ts) < 120000)) { 1779 clientRequestId = String(parsed.id); 1780 } 1781 } 1782 } catch (e) {} 1783 if (!clientRequestId) { 1784 clientRequestId = 1785 (window.crypto && typeof window.crypto.randomUUID === "function") 1786 ? window.crypto.randomUUID() 1787 : `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; 1788 try { 1789 sessionStorage.setItem(clientRequestKey, JSON.stringify({ id: clientRequestId, ts: Date.now() })); 1790 } catch (e) {} 1791 } 1792 1793 // Costruzione payload per Netlify Function 1794 const payload = { 1795 formID: "260210470291344", 1796 nome: document.getElementById('nomeRilevatore')?.value || "", 1797 email: email.value, 1798 istitutoScolastico: document.getElementById('istitutoScolastico')?.value || "", 1799 dataAvvistamento: document.getElementById('dataAvvistamento')?.value || "", 1800 cosaOsservato: cosa.value, 1801 conosciSpecieLocal: known.value, 1802 nomeSpecie: nomeSpecie?.value || "", 1803 otherSpecies: document.getElementById('otherSpecies')?.value || "", 1804 numeroIndividui: numInd.value, 1805 sesso: sessoValues, 1806 ambienteCircostante: ambienteValues, 1807 ulterioriOsservazioni: document.getElementById('ulterioriOsservazioni')?.value || "", 1808 geoloc: document.getElementById('jotformGeoloc')?.value || "",
1809 consenso: (document.querySelector('input[name="q266_ilSottoscritto"]:checked') || {}).value || "", 1810 dimensione: (document.querySelector('input[name="dimensione"]:checked') || {}).value || "", 1811 colorePredominante: document.getElementById('colorePredominante')?.value || "", 1812 1813 // Campi Extra 1814 haiOsservatoCavita, 1815 numeroCavita, 1816 numeroCivico, 1817 codiciAtlante: codiciAtlante ? codiciAtlante.split(',') : [], 1818 noteAtlante, 1819 1820 folder: "uccelli_cittadini_uploads", 1821 1822 files: filesPayload, 1823 uid, 1824 clientRequestId: clientRequestId 1825 }; 1826 1827 // --- INVIO ALLA NETLIFY FUNCTION (URL ASSOLUTO per supporto WebApp locale/cross-domain) --- 1828 const res = await fetch("/.netlify/functions/submit-jotform-cittadini", { 1829 method: "POST", 1830 headers: { "Content-Type": "application/json" }, 1831 body: JSON.stringify(payload), 1832 }); 1833 1834 if (res.ok) { 1835 1836 try { sessionStorage.removeItem(clientRequestKey); } catch (e) {} 1837 1838 // --- HISTORY SAVE (SERVER SIDE ONLY) --- 1839 // Disabilitiamo il salvataggio lato client per evitare duplicati, dato che la Netlify Function se ne occupa. 1840 // Se si volesse riattivare per feedback immediato, bisognerebbe gestire la duplicazione ID. 1841 /* 1842 try { 1843 const historyPayload = { 1844 id: Date.now().toString() + Math.random().toString(36).substr(2, 5), 1845 timestamp: new Date().toISOString(), 1846 module: "Uccelli", 1847 species: nomeSpecie?.value || "Non specificata", 1848 date: payload.dataAvvistamento || new Date().toISOString().split('T')[0], 1849 location: payload.geoloc || "Non specificata", 1850 email: payload.email || "", 1851 observer: payload.nome || "", 1852 rawPayload: payload 1853 }; 1854 1855 if (window.parent && window.parent.saveSubmissionToHistoryFirestore) { 1856 await window.parent.saveSubmissionToHistoryFirestore("Uccelli", historyPayload); 1857 } else if (window.saveSubmissionToHistoryFirestore) { 1858 await window.saveSubmissionToHistoryFirestore("Uccelli", historyPayload); 1859 } 1860 } catch(e) { 1861 console.error("Error saving history:", e); 1862 } 1863 */ 1864 // ----------------------------------- 1865 1866 // Update score (Citizens) 1867 // DISABILITATO: La Netlify Function incrementa già il punteggio. 1868 // Evitiamo il doppio incremento (+2 invece di +1). 1869 /* 1870 if (typeof incrementUserScore === 'function') { 1871 await incrementUserScore(); 1872 } 1873 */ 1874 1875 // Mostra la modale di conferma, poi ricarica la pagina per una nuova segnalazione 1876 // Funzione helper per il redirect 1877 const doRedirect = () => { 1878 const hashParams = []; 1879 hashParams.push("segnalazione_inviata=true"); 1880 hashParams.push("mapModule=" + encodeURIComponent("Uccelli (Cittadini)")); 1881 1882 if (!isNaN(lat) && !isNaN(lng)) { 1883 hashParams.push("mapGeoloc=" + encodeURIComponent(lat + "," + lng)); 1884 } 1885 1886 if (nomeSpecie && nomeSpecie.value) { 1887 hashParams.push("mapSpecies=" + encodeURIComponent(nomeSpecie.value)); 1888 } 1889 1890 const dateField = document.getElementById('dataAvvistamento'); 1891 if (dateField && dateField.value) { 1892 hashParams.push("mapDate=" + encodeURIComponent(dateField.value)); 1893 } 1894 1895 const finalHash = "#" + hashParams.join("&"); 1896 const isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1"; 1897 const homeBaseUrl = isLocal ?
1897"../index.html" : (window.location.origin + "/index.html")
1897; 1898 window.location.href = homeBaseUrl + finalHash; 1899 }; 1900 1901 const overlay = document.getElementById("successOverlay"); 1902 if (overlay) { 1903 overlay.style.display = "flex"; 1904 setTimeout(() => { 1905 doRedirect(); 1906 }, 2200); 1907 } else { 1908 alert("Grazie per la segnalazione! Il file è stato correttamente trasmesso. Il Team di GuardaMI"); 1909 doRedirect(); 1910 } 1911 } else { 1912 // Errore server 1913 let errorMessage = "Errore sconosciuto"; 1914 try { 1915 const errorData = await res.json(); 1916 errorMessage = errorData.error || errorData.message || JSON.stringify(errorData); 1917 } catch(e) {} 1918 console.error("Errore invio:", errorMessage); 1919 if (res.status === 403) { 1920 const modal = document.getElementById('outOfBoundsModal'); 1921 if (modal) { 1922 modal.style.display = 'flex'; 1923 const closeBtn = document.getElementById('closeOutOfBoundsBtn'); 1924 if (closeBtn) closeBtn.onclick = () => modal.style.display = 'none'; 1925 modal.onclick = (ev) => { 1926 if (ev.target === modal) modal.style.display = 'none'; 1927 }; 1928 } else { 1929 alert("â ï¸ Attenzione: La posizione selezionata è fuori dalla Città Metropolitana di Milano. Il modulo non può essere inviato."); 1930 } 1931 return; 1932 } 1933 alert("Errore nell'invio: " + errorMessage); 1934 } 1935 1936 } catch (err) { 1937 console.error("Errore imprevisto:", err); 1938 alert("Si è verificato un errore di connessione. Riprova."); 1939 } finally { 1940 if(realSubmitBtn) { 1941 realSubmitBtn.disabled = false; 1942 realSubmitBtn.textContent = 'Invia'; 1943 } 1944 } 1945 }); 1946 } 1947 1948 // ========= GESTIONE MODAL CODICI ATLANTE (Helper globale) ========= 1949 window.openAtlanteModal = function() { 1950 const m = document.getElementById('atlanteModal'); 1951 if(m) m.style.display = 'flex'; 1952 }; 1953 1954 window.closeAtlanteModal = function() { 1955 const m = document.getElementById('atlanteModal'); 1956 if(m) m.style.display = 'none'; 1957 1958 const checks = m.querySelectorAll('input[type="checkbox"]:checked'); 1959 const values = Array.from(checks).map(c => c.value); 1960 1961 const hiddenInput = document.getElementById('codiciAtlanteInput'); 1962 const displayDiv = document.getElementById('codiciAtlanteDisplay'); 1963 1964 if(hiddenInput) hiddenInput.value = values.join(','); 1965 1966 // Sync Note 1967 const notesArea = document.getElementById('atlanteNotes'); 1968 const notesHidden = document.getElementById('altreOsservazioniAtlante'); 1969 if(notesArea && notesHidden) { 1970 notesHidden.value = notesArea.value; 1971 } 1972 1973 if(displayDiv) { 1974 if (values.length > 0) { 1975 displayDiv.textContent = values.join(', '); 1976 displayDiv.style.display = 'block'; 1977 } else { 1978 displayDiv.textContent = ''; 1979 displayDiv.style.display = 'none'; 1980 } 1981 } 1982 }; 1983 1984 // ========= LIGHTBOX ========= 1985 let currentLightboxIndex = -1; 1986 1987 window.openLightbox = function(index) { 1988 if (typeof index !== 'number' || index < 0 || index >= selectedFiles.length) return; 1989 const item = selectedFiles[index]; 1990 if (!item || !item.file || !item.file.type.startsWith('image/')) return; 1991 1992 currentLightboxIndex = index; 1993 const lightbox = document.getElementById('lightbox'); 1994 const lightboxImage = document.getElementById('lightboxImage'); 1995 1996 // Update arrows visibility 1997 const prevBtn = document.querySelector('.lightbox-prev'); 1998 const nextBtn = document.querySelector('.lightbox-next'); 1999 const deleteBtn = document.querySelector('.lightbox-delete'); 2000 if(deleteBtn) deleteBtn.style.display = 'block'; // Ensure delete button is visible for uploads 2001 2002 let prevIdx = index - 1; 2003 while(prevIdx >= 0 && (!selectedFiles[prevIdx] || !selectedFiles[prevIdx].file.type.startsWith('image/'))) prevIdx--; 2004 2005 let nextIdx = index + 1; 2006 while(nextIdx < selectedFiles.length && (!selectedFiles[nextIdx] || !selectedFiles[nextIdx].file.type.startsWith('image/'))) nextIdx++; 2007 2008 if(prevBtn) prevBtn.style.display = (prevIdx >= 0) ? 'flex' : 'none'; 2009 if(nextBtn) nextBtn.style.display = (nextIdx < selectedFiles.length) ? 'flex' : 'none'; 2010 2011 if(lightbox && lightboxImage) { 2012 const reader = new FileReader(); 2013 reader.onload = (e) => { 2014 lightboxImage.src = e.target.result; 2015 lightbox.classList.add('visible'); 2016 };
2017 reader.readAsDataURL(item.file); 2018 } 2019 }; 2020 2021 window.openSpeciesLightbox = function(src) { 2022 const lightbox = document.getElementById('lightbox'); 2023 const lightboxImage = document.getElementById('lightboxImage'); 2024 const prevBtn = document.querySelector('.lightbox-prev'); 2025 const nextBtn = document.querySelector('.lightbox-next'); 2026 const deleteBtn = document.querySelector('.lightbox-delete'); 2027 2028 if(prevBtn) prevBtn.style.display = 'none'; 2029 if(nextBtn) nextBtn.style.display = 'none'; 2030 if(deleteBtn) deleteBtn.style.display = 'none'; 2031 2032 if(lightbox && lightboxImage) { 2033 lightboxImage.src = resolveSpeciesImagePath(src); 2034 lightbox.classList.add('visible'); 2035 } 2036 }; 2037 2038 window.closeLightbox = function() { 2039 const lightbox = document.getElementById('lightbox'); 2040 if(lightbox) { 2041 lightbox.classList.remove('visible'); 2042 } 2043 currentLightboxIndex = -1; 2044 }; 2045 2046 window.prevImage = function() { 2047 let newIndex = currentLightboxIndex - 1; 2048 while(newIndex >= 0 && (!selectedFiles[newIndex] || !selectedFiles[newIndex].file.type.startsWith('image/'))){ 2049 newIndex--; 2050 } 2051 if(newIndex >= 0) window.openLightbox(newIndex); 2052 }; 2053 2054 window.nextImage = function() { 2055 let newIndex = currentLightboxIndex + 1; 2056 while(newIndex < selectedFiles.length && (!selectedFiles[newIndex] || !selectedFiles[newIndex].file.type.startsWith('image/'))){ 2057 newIndex++; 2058 } 2059 if(newIndex < selectedFiles.length) window.openLightbox(newIndex); 2060 }; 2061 2062 window.deleteCurrentLightboxImage = function() { 2063 if(currentLightboxIndex > -1) { 2064 window.removeFile(currentLightboxIndex); 2065 window.closeLightbox(); 2066 } 2067 }; 2068 2069 // Close lightbox when clicking outside the image 2070 const lightbox = document.getElementById('lightbox'); 2071 if(lightbox) { 2072 lightbox.addEventListener('click', function(e) { 2073 if(e.target === lightbox) { 2074 closeLightbox(); 2075 } 2076 }); 2077 } 2078 2079 // Initialize blocks 2080 updateBlocks(); 2081 2082 // Funzione per aprire/chiudere i box info 2083 window.toggleInfo = function(id) { 2084 const box = document.getElementById(id); 2085 if (box) { 2086 box.classList.toggle('visible'); 2087 } 2088 }; 2089 2090 // ========= SUBMIT CONSENT LOGIC ========= 2091 const consentSubmitBtn = document.getElementById('submitButton'); 2092 const consentRadiosGroup = document.querySelectorAll('input[name="q266_ilSottoscritto"]'); 2093 2094 function updateSubmitState() { 2095 if(!consentSubmitBtn) return; 2096 2097 const consentValue = (document.querySelector('input[name="q266_ilSottoscritto"]:checked') || {}).value; 2098 const ageGate = document.getElementById('ageGate14'); 2099 const ageOk = (ageGate && ageGate.checked) || localStorage.getItem(AGE_GATE_KEY) === "true"; 2100 2101 if (consentValue === 'Acconsento' && ageOk) { 2102 consentSubmitBtn.disabled = false; 2103 consentSubmitBtn.style.background = '#1d4ed8'; // Blue 2104 consentSubmitBtn.style.cursor = 'pointer'; 2105 } else { 2106 consentSubmitBtn.disabled = true; 2107 consentSubmitBtn.style.background = '#9ca3af'; // Gray 2108 consentSubmitBtn.style.cursor = 'not-allowed'; 2109 } 2110 } 2111 2112 // Init immediately 2113 updateSubmitState(); 2114 2115 // Listeners 2116 consentRadiosGroup.forEach(r => { 2117 r.addEventListener('change', updateSubmitState); 2118 }); 2119 2120})(); 2121 2122async function incrementUserScore() { 2123 // 1. Try Modular SDK (window.fs) - PREFERRED 2124 if (window.firebaseAuth && window.firebaseDb && window.fs && window.fs.updateDoc && window.fs.increment) { 2125 const user = window.firebaseAuth.currentUser; 2126 if (!user) { 2127 console.warn("User not logged in (Modular), score not updated."); 2128 return; 2129 } 2130 2131 const userRef = window.fs.doc(window.firebaseDb, "users", user.uid); 2132 try { 2133 await window.fs.updateDoc(userRef, { 2134 points: window.fs.increment(1) 2135 }); 2136 console.log("Score updated successfully +1 (Modular)"); 2137 return; 2138 } catch (e) { 2139 console.error("Score update failed (Modular):", e); 2140 // Retry with setDoc if doc missing 2141 if (e.code === 'not-found' || e.message.includes("No document to update")) { 2142 try { 2143 await window.fs.setDoc(userRef, { points: 1 }, { merge: true }); 2144 console.log("Score set successfully (Modular)"); 2145 } catch (err) { 2146 console.error("Retry set points failed (Modular):", err); 2147 } 2148 } 2149 return; 2150 } 2151 } 2152} 2153 2154 2155 const istitutoScolastico = document.getElementById('istitutoScolastico'); 2156 if (istitutoScolastico) { 2157 const scuoleList = document.getElementById('scuoleList'); 2158 const normalize = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim(); 2159 const updateSuggestions = () => { 2160 if (!scuoleList || typeof SCUOLE_MILANO === 'undefined') return; 2161 const q = normalize(istitutoScolastico.value); 2162 scuoleList.innerHTML = ''; 2163 if (!q) return; 2164 let added = 0; 2165 for (const scuola of SCUOLE_MILANO) { 2166 if (normalize(scuola).includes(q)) { 2167 const option = document.createElement('option'); 2168 option.value = scuola;
2169 scuoleList.appendChild(option); 2170 added++; 2171 if (added >= 30) break; 2172 } 2173 } 2174 }; 2175 2176 const savedIstituto = localStorage.getItem('guardami_istituto'); 2177 if (savedIstituto) { 2178 istitutoScolastico.value = savedIstituto; 2179 } 2180 2181 istitutoScolastico.addEventListener('input', () => { 2182 localStorage.setItem('guardami_istituto', istitutoScolastico.value); 2183 updateSuggestions(); 2184 }); 2185 istitutoScolastico.addEventListener('focus', updateSuggestions); 2186 }
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.