@@ -8,6 +8,15 @@ Parla con le rotte dello stesso servizio: /camera/sources, /camera/anteprima,
/camera/parametri, /camera/parametri/salva, /camera/calibration, /camera/calibrate.
#84: calibrazione densa, un solo dato dell ' operatore (lato dello scacco in mm).
#87: anteprima, parametri camera, stile TMF.
#91: due tab, «Acquisizione» e «Calibrazione», nella stessa pagina; l ' anteprima resta
visibile in entrambe. Calibrazioni salvate con nome in un archivio unico
(/camera/calibrazioni, /camera/calibrazione-attiva); il calcolo non salva piu ' :
si salva dal calcolo_id con un nome.
#92: l ' immagine da file e ' una SORGENTE di acquisizione (POST /camera/immagini), non
un ' opzione della calibrazione: la calibrazione usa sempre il frame grezzo della sorgente
scelta nella tab Acquisizione. Sorgente in cima alla tab; riquadri «Immagine» (grezza) e,
con la casella, «Calibrata» (/camera/anteprima/calibrata) affiancati; il ciclo anteprima
si sospende mentre la tab Calibrazione e ' aperta.
Dipende da: niente. E ' una stringa, cosi ' il pacchetto compilato non ha file dati.
"""
@@ -57,10 +66,19 @@ PAGINA = r"""<!doctype html>
display: flex; align-items: center; justify-content: center; margin-top: 10px; overflow: hidden; }
.cornice { position: relative; display: inline-block; line-height: 0; max-width: 100 % ; }
.cornice img { max-width: 100 % ; max-height: calc(100vh - 330px); min-height: 200px; border: 0; margin: 0; }
# mirino { position: absolute; inset: 0; pointer-events: none;
. mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100 % no-repeat,
linear-gradient(#fbbf24, #fbbf24) center / 100 % 2px no-repeat; opacity: .85; }
#vuoto { padding: 30px 16px; color: var(--secondario); text-align: center; }
#vuoto, #vuotoCal { padding: 30px 16px; color: var(--secondario); text-align: center; }
/* #92: due riquadri affiancati (grezza | calibrata); in colonna sotto i 900 px */
.riquadri { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; }
.riquadri.doppio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.riquadro .titolo { font-size: .88rem; font-weight: 600; }
.riquadro .titolo span { font-weight: 400; font-size: .78rem; color: var(--secondario); margin-left: 6px; }
.riquadro .palco { margin-top: 4px; }
.sorgente-riga { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.sorgente-riga .campo-sorgente { flex: 1 1 260px; min-width: 0; }
.sorgente-riga select { width: 100 % ; }
.indicatori { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 10px; }
.indicatore { background: var(--sfondo); border: 1px solid var(--bordo); border-radius: 6px; padding: 8px 10px; }
.indicatore .nome { font-size: .78rem; color: var(--secondario); }
@@ -75,33 +93,91 @@ PAGINA = r"""<!doctype html>
.parametro .campo input[type=text] { width: 110px; text-align: right; flex: none; }
.parametro .unita { font-size: .82rem; color: var(--secondario); min-width: 2.2em; }
.parametro .limiti { font-size: .75rem; color: var(--secondario); }
/* #91: tab Acquisizione | Calibrazione */
.tabs { display: flex; gap: 6px; margin: 0 0 14px; border-bottom: 1px solid var(--bordo); }
.tabs button { background: transparent; color: var(--secondario); border-radius: 6px 6px 0 0; border-bottom: 3px solid transparent;
flex: 0 1 auto; padding: 8px 18px; }
.tabs button[aria-selected=true] { color: var(--testo); background: var(--pannello); border-bottom-color: var(--accento); font-weight: 600; }
/* #91: calibrazione con nome, elenco delle salvate */
/* #91: elenco a schede, una per calibrazione; nome e risoluzione sempre visibili, niente scorrimento orizzontale */
.elenco { margin-top: 8px; }
.scheda-cal { border-top: 1px solid var(--bordo); padding: 8px 0; }
.scheda-cal .nome-cal { font-weight: 700; overflow-wrap: anywhere; }
.scheda-cal .dettagli { font-size: .8rem; color: var(--secondario); margin-top: 2px; overflow-wrap: anywhere; }
.scheda-cal .azioni { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.scheda-cal .azioni button { padding: 3px 10px; font-size: .82rem; }
.segno { display: inline-block; font-size: .72rem; padding: 0 6px; border-radius: 9px; background: var(--ok); color: #052e16; margin-left: 6px; }
.segno.diversa { background: var(--avviso); color: #422006; }
@media (max-width: 900px) {
.griglia { grid-template-columns: minmax(0, 1fr); }
.riquadri.doppio { grid-template-columns: minmax(0, 1fr); } /* #92: in colonna */
.cornice img { max-height: 60vh; }
}
@media (max-width: 500px) { /* #91: utilizzabile anche stretta (400 px) */
main { padding: 8px; }
.scheda { padding: 10px; }
.tabs button { flex: 1 1 0; padding: 8px 6px; }
.parametro .campo input[type=text] { width: 88px; }
}
</style>
</head>
<body>
<main>
<h1>Acquisizione camera</h1>
<div class= " griglia " >
<div class=" colonna " >
<!-- #87: sezione Anteprima -- >
<!-- #91: due tab; #92: l ' anteprima sta nella tab Acquisizione e si sospende nella tab Calibrazione -- >
<div class= " tabs " role= " tablist " aria-label= " Sezioni della pagina " >
<button id= " tabAcq " type= " button " role= " tab " aria-selected= " true " aria-controls= " pannelloAcq " >Acquisizione</button >
<button id= " tabCal " type= " button " role= " tab " aria-selected= " false " aria-controls= " pannelloCal " tabindex= " -1 " >Calibrazione</button>
</div>
<div class= " colonna " id= " pannelloAcq " role= " tabpanel " aria-labelledby= " tabAcq " >
<!-- #92: Sorgente in cima, a larghezza piena: camere, cartella e immagini caricate da file -->
<section class= " scheda " id= " sezioneSorgente " >
<h2>Sorgente</h2>
<div class= " sorgente-riga " >
<div class= " campo-sorgente " >
<label for= " sorgente " >Camera o immagine</label>
<select id= " sorgente " ></select>
</div>
<button id= " caricaImmagine " class= " secondario " type= " button " >Carica immagine da file...</button>
<input id= " fileImmagine " type= " file " accept= " image/* " hidden>
<button id= " rimuoviImmagine " class= " secondario " type= " button " hidden>Rimuovi immagine</button>
</div>
<div class= " stato " id= " statoSorgente " >-</div>
<div id= " msgSorgente " class= " messaggio " ></div>
</section>
<!-- #87: sezione Anteprima; #92: barra, due riquadri affiancati (grezza | calibrata), indicatori dal frame grezzo -->
<section class= " scheda " id= " sezioneAnteprima " >
<h2>Anteprima</h2>
<div class= " riga " >
<button id= " avvia " >Avvia anteprima</button>
<button id= " ferma " class= " secondario " disabled>Ferma anteprima</button>
<button id= " scatta " class= " secondario " >Scatta</button>
<label style= " margin:0 0 6px " ><input id= " grezzo " type= " checkbox " > Immagine grezza (senza correzione) </label>
<label style= " margin:0 0 6px " ><input id= " mostraCal " type= " checkbox " > Mostra immagine calibrata </label>
<label style= " margin:0 0 6px " ><input id= " mirinoCasella " type= " checkbox " > Mirino centrale</label>
</div>
<div class= " riquadri " id= " riquadri " >
<div class= " riquadro " >
<div class= " titolo " >Immagine<span>grezza, senza correzione</span></div>
<div class= " palco " >
<div id= " vuoto " >Anteprima ferma. Premere «Avvia anteprima» o «Scatta».</div>
<div class= " cornice " id= " cornice " hidden>
<img id= " immagine " alt= " Anteprima della camera " >
<div id= " mirino " hidden></div>
<img id= " immagine " alt= " Anteprima grezza della sorgente " >
<div id= " mirino " class= " mirino " hidden></div>
</div>
</div>
</div>
<div class= " riquadro " id= " riquadroCal " hidden>
<div class= " titolo " >Calibrata<span id= " nomeCalibrata " ></span></div>
<div class= " palco " >
<div id= " vuotoCal " >Premere «Avvia anteprima» o «Scatta».</div>
<div class= " cornice " id= " corniceCal " hidden>
<img id= " immagineCal " alt= " Anteprima della sorgente raddrizzata dalla calibrazione " >
<div id= " mirinoCal " class= " mirino " hidden></div>
</div>
</div>
</div>
</div>
<div class= " indicatori " >
@@ -130,17 +206,6 @@ PAGINA = r"""<!doctype html>
</div>
<div id= " msgAnteprima " class= " messaggio " ></div>
</section>
</div>
<div class= " colonna " >
<!-- #87: sezione Sorgente -->
<section class= " scheda " id= " sezioneSorgente " >
<h2>Sorgente</h2>
<label for= " sorgente " >Camera o cartella</label>
<select id= " sorgente " ></select>
<div class= " stato " id= " statoSorgente " >-</div>
<div id= " msgSorgente " class= " messaggio " ></div>
</section>
<!-- #87: sezione Parametri camera -->
<section class= " scheda " id= " sezioneParametri " >
@@ -151,45 +216,43 @@ PAGINA = r"""<!doctype html>
</div>
<div id= " msgParametri " class= " messaggio " ></div>
</section>
</div>
<!-- #91: tab Calibrazione; #92: sempre sul frame grezzo della sorgente scelta in Acquisizione, niente file qui -->
<div class= " griglia " id= " pannelloCal " role= " tabpanel " aria-labelledby= " tabCal " hidden>
<section class= " scheda " id= " sezioneCalibrazione " >
<h2>Calibrazione</h2>
<div class= " riga " >
<div class= " stato " style= " margin:0 0 10px " >Sorgente: <strong id= " calSorgente " >-</strong> — immagine grezza dalla tab Acquisizione</div >
<div class= " riga " style= " margin-top:10px " >
<div><label for= " quadrato " >Lato dello scacco (mm)</label><input id= " quadrato " type= " text " inputmode= " decimal " value= " 2 " size= " 8 " ></div>
<button id= " acquisisci " title= " Acquisisci e calibra " >Calcola</button>
</div>
<p class= " nota " >
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
La griglia si trova da sola; il risultato corregge anche la distorsione dell ' ottica.
Durante il calcolo l ' anteprima si sospende .</p>
<div class= " riga " style= " margin-top:10px " >
<button id= " acquisisci " >Acquisisci e calibra</button>
<button id= " scegliFile " class= " secondario " >Scegli immagine...</button>
<input id= " file " type= " file " accept= " image/* " hidden>
<span id= " nomeFile " class= " nota " style= " margin:0 0 6px " >nessuna immagine scelta</span>
<button id= " carica " class= " secondario " >Carica immagine e calibra</button>
</div>
Il frame è sempre quello grezzo della sorgente; l ' anteprima resta sospesa finché questa tab è aperta .</p>
<div id= " messaggio " class= " messaggio " ></div>
<div id= " risultato " hidden style= " margin-top:10px " >
<strong>Risultato del calcolo</strong>
<div id= " origineCalcolo " class= " nota " style= " margin:2px 0 6px " ></div>
<table id= " tabella " ></table>
<div id= " avviso " class= " avviso " ></div>
<div class= " riga " style= " margin-top:10px " >
<div><label for= " nomeCal " >Nome</label><input id= " nomeCal " type= " text " maxlength= " 60 " size= " 22 " autocomplete= " off " ></div>
<button id= " salva " >Salva</button>
<span class= " nota " style= " margin:0 0 6px " >Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.</span>
</div>
<div id= " msgSalva " class= " messaggio " ></div>
<img id= " overlay " alt= " Angoli trovati sulla scacchiera " style= " max-width:100 % ;margin-top:8px;border:1px solid var(--bordo);border-radius:6px " >
</div>
<div style= " margin-top:14px " >
<strong>Calibrazione salvata</strong>
<div id= " stato " class= " stato " >-</div>
<div class= " riga " style= " margin-top:10px " >
<button id= " elimina " class= " pericolo " disabled>Elimina calibrazione</button>
</div>
</div>
</section>
</div>
<section class= " scheda " id= " sezioneSalvate " >
<h2>Calibrazioni salvate</h2>
<div id= " stato " class= " stato " >-</div>
<div id= " calibrazioni " class= " elenco " ></div> <!-- #91: una scheda per calibrazione, niente tabella larga -->
<div id= " msgElenco " class= " messaggio " ></div>
</section>
</div>
</main>
@@ -248,8 +311,11 @@ async function dettaglio(risposta) {
// ---- Sorgente
async function caricaSorgenti() {
// #92: scegli = nome da selezionare (immagine appena caricata); senza, l ' ultima scelta ricordata
async function caricaSorgenti(scegli) {
var sel = $( " sorgente " );
var prima = sel.value;
sel.textContent = " " ;
try {
var r = await fetch( " /camera/sources " );
var j = await r.json();
@@ -257,24 +323,86 @@ async function caricaSorgenti() {
var o = document.createElement( " option " );
o.value = s.name;
o.textContent = s.label || s.name;
o.dataset.kind = s.kind || " " ; // #92: «image» = immagine caricata da file
sel.appendChild(o);
});
} catch (e) { dire( " msgSorgente " , " Elenco delle sorgenti non disponibile. " , " errore " ); }
if (!sel.options.length) dire( " msgSorgente " , " Nessuna sorgente camera trovata. " , " errore " );
var ricordata = memoria( " tmf.acq.sorgente " ); // #87: ultima scelta ricordata
if (ricorda ta) {
for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === ricorda ta) sel.value = ricorda ta;
var voluta = scegli || (prima === " " ? memoria(" tmf.acq.sorgente " ) : prima) ; // #87: ultima scelta ricordata
if (volu ta) {
for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === volu ta) sel.value = volu ta;
}
cambioSorgente(false );
cambioSorgente(!!scegli );
}
// #92: la sorgente selezionata e ' un ' immagine caricata (si puo ' rimuovere)?
function sorgenteImmagine() {
var sel = $( " sorgente " );
return sel.selectedIndex >= 0 && sel.options[sel.selectedIndex].dataset.kind === " image " ;
}
// #92: «Carica immagine da file...» -> POST /camera/immagini, poi la nuova immagine diventa la sorgente
$( " caricaImmagine " ).onclick = function () { $( " fileImmagine " ).click(); };
$( " fileImmagine " ).onchange = async function () {
var file = $( " fileImmagine " ).files[0];
$( " fileImmagine " ).value = " " ; // lo stesso file si puo ' ricaricare
if (!file) return;
var f = new FormData();
f.append( " image " , file);
$( " caricaImmagine " ).disabled = true;
dire( " msgSorgente " , " Caricamento di « " + file.name + " »... " , " " );
try {
var r = await fetch( " /camera/immagini " , { method: " POST " , body: f });
if (!r.ok) { dire( " msgSorgente " , await dettaglio(r), " errore " ); return; }
var j = await r.json();
await caricaSorgenti(j.source);
dire( " msgSorgente " , " Immagine « " + j.source + " » caricata: premere «Scatta» per vederla. " , " ok " );
} catch (e) {
dire( " msgSorgente " , " Servizio non raggiungibile. " , " errore " );
} finally {
$( " caricaImmagine " ).disabled = false;
}
};
// #92: «Rimuovi immagine» con doppio clic entro 4 s (niente finestre del browser)
var R = { timer: null };
function annullaRimuovi() {
clearTimeout(R.timer);
R.timer = null;
$( " rimuoviImmagine " ).textContent = " Rimuovi immagine " ;
}
$( " rimuoviImmagine " ).onclick = async function () {
var nome = $( " sorgente " ).value;
if (!nome || !sorgenteImmagine()) return;
if (R.timer === null) {
$( " rimuoviImmagine " ).textContent = " Confermi? Clic ancora " ;
R.timer = setTimeout(annullaRimuovi, 4000);
return;
}
annullaRimuovi();
fermaAnteprima(); // la sorgente sta per sparire: niente richieste in volo su di lei
while (A.inVolo) await pausa(50);
try {
var r = await fetch( " /camera/immagini?nome= " + encodeURIComponent(nome), { method: " DELETE " });
if (!r.ok) { dire( " msgSorgente " , await dettaglio(r), " errore " ); return; }
await caricaSorgenti( " " );
dire( " msgSorgente " , " Immagine « " + nome + " » rimossa. " , " ok " );
} catch (e) {
dire( " msgSorgente " , " Servizio non raggiungibile. " , " errore " );
}
};
// #87: cambiando sorgente si azzerano gli indicatori e si rileggono parametri e calibrazione
function cambioSorgente(salva) {
if (salva) memoria( " tmf.acq.sorgente " , $( " sorgente " ).value);
var era = A.attiva;
fermaAnteprima();
azzeraIndicatori();
svuotaRiquadri(); // #92: il frame vecchio non e ' della nuova sorgente
annullaRimuovi();
$( " rimuoviImmagine " ).hidden = !sorgenteImmagine();
dire( " msgAnteprima " , " " );
dire( " msgElenco " , " " ); // #91
aggiornaStato();
caricaParametri();
if (era) avviaAnteprima();
@@ -282,7 +410,8 @@ function cambioSorgente(salva) {
// ---- Anteprima (#87)
var A = { attiva: f alse, gen: 0, inVolo: false, url: null, nitMax: 0 };
// #91: ris = { w, h} dell ' ultimo frame; #92: urlC al/ seqCal = riquadro calibrata, riprendi = ciclo sospeso dalla tab Calibrazione, lato = ultimo lato_max
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, riprendi: false, lato: 1280, nitMax: 0, ris: null };
function azzeraIndicatori() {
A.nitMax = 0;
@@ -290,6 +419,8 @@ function azzeraIndicatori() {
$( " saturati " ).className = " valore " ;
$( " barraNitidezza " ).style.width = " 0 " ;
$( " raddrizzato " ).textContent = " " ;
A.ris = null; // #91: risoluzione del frame non piu ' nota
segnaRisoluzioni();
}
function pulsantiAnteprima() {
@@ -306,6 +437,52 @@ function mostraImmagine(blob) {
$( " vuoto " ).hidden = true;
}
// #92: riquadro «Calibrata»: l ' immagine raddrizzata oppure il motivo per cui non c ' e ' (404/409 del servizio)
function mostraCalibrata(blob, nome) {
var vecchio = A.urlCal;
A.urlCal = URL.createObjectURL(blob);
$( " immagineCal " ).src = A.urlCal;
if (vecchio) URL.revokeObjectURL(vecchio);
$( " corniceCal " ).hidden = false;
$( " vuotoCal " ).hidden = true;
$( " nomeCalibrata " ).textContent = nome ? " « " + nome + " » " : " " ;
}
function messaggioCalibrata(testo) {
$( " corniceCal " ).hidden = true;
$( " vuotoCal " ).hidden = false;
$( " vuotoCal " ).textContent = testo;
$( " nomeCalibrata " ).textContent = " " ;
}
// #92: cambio sorgente: via i due frame, nessuna immagine di un ' altra sorgente
function svuotaRiquadri() {
A.seqCal++; // una risposta calibrata ancora in volo non vale piu '
$( " cornice " ).hidden = true;
$( " vuoto " ).hidden = false;
messaggioCalibrata( " Premere «Avvia anteprima» o «Scatta». " );
}
// #92: raddrizza l ' ULTIMO frame grezzo tenuto dal servizio (nessuna lettura nuova); chiamata dopo ogni anteprima, dopo Carica/Scollega e alla casella
async function aggiornaCalibrata(lato) {
var sorgente = $( " sorgente " ).value;
if (!$( " mostraCal " ).checked || !sorgente) return;
var n = ++A.seqCal;
try {
var r = await fetch( " /camera/anteprima/calibrata " , {
method: " POST " ,
headers: { " Content-Type " : " application/json " },
body: JSON.stringify( { source: sorgente, lato_max: lato || A.lato })
});
if (n !== A.seqCal) return; // risposta superata da una piu ' recente o da un cambio sorgente
if (!r.ok) { messaggioCalibrata(await dettaglio(r)); return; } // l ' anteprima grezza continua
var nome = " " ;
try { nome = decodeURIComponent(r.headers.get( " X-Calibrazione " ) || " " ); } catch (e) { /* intestazione non decodificabile: nessun nome */ }
var blob = await r.blob();
if (n !== A.seqCal) return;
mostraCalibrata(blob, nome);
} catch (e) {
if (n === A.seqCal) messaggioCalibrata( " Servizio non raggiungibile. " );
}
}
function aggiornaIndicatori(h) {
var nit = parseFloat(h.get( " X-Nitidezza " ));
var sat = parseFloat(h.get( " X-Saturati-Pct " ));
@@ -322,9 +499,13 @@ function aggiornaIndicatori(h) {
}
if (isFinite(media)) $( " media " ).textContent = fmt(media, 1);
var w = h.get( " X-Larghezza " ), a = h.get( " X-Altezza " );
if (w && a) $( " dimensioni " ).textContent = fmt(w, 0) + " x " + fmt(a, 0);
var r = h.get( " X-Raddrizzato " );
$( " raddrizzato " ).textContent = r === " si " ? " raddrizzata dalla calibrazione " : r === " no " ? " non raddrizzata " : " " ;
if (w && a) {
$( " dimensioni " ).textContent = fmt(w, 0) + " x " + fmt(a, 0 );
// #91: risoluzione nota del frame, per segnare le calibrazioni che non combaciano
var nw = parseInt(w, 10), na = parseInt(a, 10);
if (!A.ris || A.ris.w !== nw || A.ris.h !== na) { A.ris = { w: nw, h: na }; segnaRisoluzioni(); }
}
$( " raddrizzato " ).textContent = " frame grezzo " ; // #92: l ' indicatore viene sempre dal frame grezzo
}
// una richiesta di anteprima; ritorna " ok " | " errore " | " fatale " (errore che non passa riprovando)
@@ -332,12 +513,13 @@ async function unaAnteprima(lato) {
var sorgente = $( " sorgente " ).value;
if (!sorgente) { dire( " msgAnteprima " , " Scegliere una sorgente. " , " errore " ); return " fatale " ; }
var g = A.gen;
A.lato = lato;
A.inVolo = true;
try {
var r = await fetch( " /camera/anteprima " , {
method: " POST " ,
headers: { " Content-Type " : " application/json " },
body: JSON.stringify( { source: sorgente, grezzo: $( " grezzo " ).checked , lato_max: lato, timeout_ms: 5000 })
body: JSON.stringify( { source: sorgente, grezzo: true , lato_max: lato, timeout_ms: 5000 }) // #92: il riquadro «Immagine» e ' sempre grezzo
});
if (!r.ok) {
var d = await dettaglio(r);
@@ -349,6 +531,7 @@ async function unaAnteprima(lato) {
mostraImmagine(blob);
aggiornaIndicatori(r.headers);
dire( " msgAnteprima " , " " );
await aggiornaCalibrata(lato); // #92: stesso frame, raddrizzato; resta in volo finche ' non risponde
return " ok " ;
} catch (e) {
if (g === A.gen) dire( " msgAnteprima " , " Servizio non raggiungibile. " , " errore " );
@@ -398,7 +581,15 @@ $("avvia").onclick = avviaAnteprima;
$( " ferma " ).onclick = fermaAnteprima;
$( " scatta " ).onclick = scatta;
$( " azzeraMax " ).onclick = function () { A.nitMax = 0; $( " nitidezzaMax " ).textContent = " - " ; $( " barraNitidezza " ).style.width = " 0 " ; };
$( " mirinoCasella " ).onchange = function () { $( " mirino " ).hidden = !$( " mirinoCasella " ).checked; };
// #92: il mirino vale per entrambi i riquadri; la casella «Mostra immagine calibrata» apre il riquadro e raddrizza il frame gia ' tenuto
$( " mirinoCasella " ).onchange = function () { $( " mirino " ).hidden = $( " mirinoCal " ).hidden = !$( " mirinoCasella " ).checked; };
function mostraRiquadroCal() {
var si = $( " mostraCal " ).checked;
$( " riquadroCal " ).hidden = !si;
$( " riquadri " ).className = " riquadri " + (si ? " doppio " : " " );
if (si) aggiornaCalibrata();
}
$( " mostraCal " ).onchange = mostraRiquadroCal;
// ---- Parametri camera (#87)
@@ -439,6 +630,12 @@ async function caricaParametri() {
$( " salvaParametriRiga " ).hidden = true;
dire( " msgParametri " , " " );
if (!sorgente) { $( " parametri " ).textContent = " - " ; return; }
// #91: subito via il testo della sorgente precedente; la prima apertura della camera puo ' durare ~20 s
$( " parametri " ).textContent = " " ;
var attesa = document.createElement( " div " );
attesa.className = " nota " ;
attesa.textContent = " Lettura dei parametri... " ;
$( " parametri " ).appendChild(attesa);
try {
var r = await fetch( " /camera/parametri?source= " + encodeURIComponent(sorgente));
if (g !== P.gen) return;
@@ -611,6 +808,33 @@ $("salvaParametri").onclick = async function () {
}
};
// ---- Tab (#91): «Acquisizione» | «Calibrazione»; l ' anteprima sta fuori dai pannelli e resta sempre visibile
function scegliTab(nome, salva) {
var cal = nome === " cal " ;
$( " tabAcq " ).setAttribute( " aria-selected " , cal ? " false " : " true " );
$( " tabCal " ).setAttribute( " aria-selected " , cal ? " true " : " false " );
$( " tabAcq " ).tabIndex = cal ? -1 : 0;
$( " tabCal " ).tabIndex = cal ? 0 : -1;
$( " pannelloAcq " ).hidden = cal;
$( " pannelloCal " ).hidden = !cal;
// #92: nella tab Calibrazione il ciclo anteprima si sospende; tornando in Acquisizione riprende se era attivo
if (cal && A.attiva) { A.riprendi = true; fermaAnteprima(); }
if (!cal && A.riprendi) { A.riprendi = false; avviaAnteprima(); }
if (!cal && salva) aggiornaCalibrata(); // il riquadro calibrata riflette la calibrazione attiva di adesso
if (salva) { memoria( " tmf.acq.tab " , nome); if (cal) caricaCalibrazioni(); } // l ' elenco si rilegge entrando
}
$( " tabAcq " ).onclick = function () { scegliTab( " acq " , true); };
$( " tabCal " ).onclick = function () { scegliTab( " cal " , true); };
[$( " tabAcq " ), $( " tabCal " )].forEach(function (b) { // frecce sinistra/destra fra le tab
b.onkeydown = function (e) {
if (e.key !== " ArrowLeft " && e.key !== " ArrowRight " ) return;
var cal = b.id === " tabAcq " ;
scegliTab(cal ? " cal " : " acq " , true);
$(cal ? " tabCal " : " tabAcq " ).focus();
};
});
// ---- Calibrazione (logica di #83 e #84; #87: lato precompilato, data italiana, conferma in pagina)
function righe(d) {
@@ -644,37 +868,6 @@ function tabella(el, d) {
});
}
async function aggiornaStato() {
var sorgente = $( " sorgente " ).value;
var stato = $( " stato " );
annullaConferma();
$( " elimina " ).disabled = true;
if (!sorgente) { stato.textContent = " - " ; $( " statoSorgente " ).textContent = " - " ; return; }
try {
var r = await fetch( " /camera/calibration?source= " + encodeURIComponent(sorgente));
if (sorgente !== $( " sorgente " ).value) return; // nel frattempo si e ' cambiata sorgente
if (r.status === 404) {
stato.textContent = " Sorgente non calibrata. " ;
$( " statoSorgente " ).textContent = " non calibrata " ;
return;
}
if (!r.ok) { stato.textContent = await dettaglio(r); return; }
var d = await r.json();
stato.textContent = " " ;
var t = document.createElement( " table " );
tabella(t, d);
var quando = document.createElement( " div " );
quando.textContent = " Calibrata il " + dataItaliana(d.calibrated_at);
stato.appendChild(t);
stato.appendChild(quando);
$( " elimina " ).disabled = false;
$( " statoSorgente " ).textContent = " calibrata: " + fmt(d.px_per_mm, 2) + " px/mm " ;
if (d.square_mm) $( " quadrato " ).value = fmtCasella(d.square_mm); // #87: parte dal lato gia ' usato
} catch (e) {
stato.textContent = " Servizio non raggiungibile. " ;
}
}
// avvisi sulla qualita ' (solo calibrazione densa): copertura bassa o residuo alto
function avvisi(d) {
var a = [];
@@ -687,78 +880,273 @@ function avvisi(d) {
return a.join( " " );
}
async function calcola(conFile, salva) {
// ---- Calibrazioni con nome (#91): archivio unico, una attiva per sorgente
var C = { elenco: [], attiva: null, celle: [], gen: 0, confermaNome: null, confermaBtn: null, confermaTimer: null };
var S = { nome: null, timer: null }; // secondo clic su «Sovrascrivi» entro 4 s
var JSON_H = { " Content-Type " : " application/json " };
function descriviOrigine(o) { return String(o || " - " ).replace( " : " , " " ); }
// stato della calibrazione attiva sulla sorgente selezionata (come prima di #91, piu ' il nome)
async function aggiornaStato() {
var sorgente = $( " sorgente " ).value;
if (!sorgente) { messaggio( " Scegliere una sorgente. " , " errore " ); return; }
var lato = leggiNumero($( " quadrato " ).value );
if (!(lato > 0)) { messaggio( " Il lato dello scacco deve essere un numero maggiore di zero. " , " errore " ) ; return; }
var f = new FormData();
f.append( " source " , sorgente);
f.append( " square_mm " , String(lato)); // al servizio sempre con il punto
f.append( " save " , salva ? " true " : " false " );
if (conFile) {
var file = $( " file " ).files[0];
if (!file) { messaggio( " Scegliere un file immagine. " , " errore " ); return; }
f.append( " image " , file);
}
var eraAttiva = A.attiva; // #87: l ' anteprima si sospende e poi riprende
if (eraAttiva) fermaAnteprima();
messaggio( " Calcolo in corso... " , " " );
[ " acquisisci " , " carica " , " salva " ].forEach(function (i) { $(i).disabled = true; });
var stato = $( " stato " );
annullaConferma( );
$( " calSorgente " ).textContent = sorgente ? $( " sorgente " ).options[$( " sorgente " ).selectedIndex].textContent : " - " ; // #91
if (!sorgente) { stato.textContent = " - " ; $( " statoSorgente " ).textContent = " - " ; caricaCalibrazioni(); return; }
try {
var r = await fetch( " /camera/calibrate " , { method: " POST " , body: f } );
if (!r.ok) { messaggio(await dettaglio(r), " errore " ); return; }
var r = await fetch( " /camera/calibration?source= " + encodeURIComponent(sorgente) );
if (sorgente !== $( " sorgente " ).value) return; // nel frattempo si e ' cambiata sorgente
if (r.status === 404) {
stato.textContent = " Sorgente non calibrata. " ;
$( " statoSorgente " ).textContent = " non calibrata " ;
} else if (!r.ok) {
stato.textContent = await dettaglio(r);
} else {
var d = await r.json();
ultimo = { conFile: conFile, dati: d } ;
tabella($ ( " tabella " ), d) ;
$( " overlay " ).src = " data:image/png;base64, " + d.overlay_png ;
$( " avviso " ).textContent = avvisi(d) ;
$ ( " risultato " ).hidden = false ;
messaggio(d.saved ? " Calibrazione salvata. " : " Calibrazione riuscita: controllare e salvare. " , " ok " );
if (d.saved) aggiornaStato( );
stato.textContent = " " ;
var titolo = document.createElement ( " div " );
titolo.textContent = " Calibrazione attiva: " + (d.nome ? " « " + d.nome + " » " : " (senza nome) " ) ;
titolo.style.fontWeight = " 600 " ;
var t = document.createElement ( " table " );
tabella(t, d );
var quando = document.createElement( " div " );
quando.textContent = " Calibrata il " + dataItaliana(d.calibrated_at);
stato.appendChild(titolo);
stato.appendChild(t);
stato.appendChild(quando);
$( " statoSorgente " ).textContent = " calibrata: " + (d.nome || " (senza nome) " ) + " · " + fmt(d.px_per_mm, 2) + " px/mm " ; // #92
if (d.square_mm) $( " quadrato " ).value = fmtCasella(d.square_mm); // #87: parte dal lato gia ' usato
}
} catch (e) {
messaggio( " Servizio non raggiungibile. " , " errore " ) ;
} finally {
[ " acquisisci " , " carica " , " salva " ].forEach(function (i) { $(i).disabled = false; } );
if (eraAttiva) avviaAnteprima();
stato.textContent = " Servizio non raggiungibile. " ;
}
caricaCalibrazioni( );
}
// #91: elenco unico delle calibrazioni salvate; " attiva " e ' quella della sorgente selezionata
async function caricaCalibrazioni() {
var sorgente = $( " sorgente " ).value;
var g = ++C.gen;
try {
var r = await fetch( " /camera/calibrazioni " + (sorgente ? " ?source= " + encodeURIComponent(sorgente) : " " ));
if (g !== C.gen) return;
if (!r.ok) { dire( " msgElenco " , await dettaglio(r), " errore " ); return; }
var j = await r.json();
if (g !== C.gen) return;
C.elenco = j.calibrazioni || [];
C.attiva = j.attiva || null;
disegnaElenco();
} catch (e) {
if (g === C.gen) dire( " msgElenco " , " Servizio non raggiungibile. " , " errore " );
}
}
$( " acquisisci " ).onclick = function () { calcola(false, false); };
$( " carica " ).onclick = function () { calcola(true, false); } ;
// «Salva» rifa il calcolo con save=true sulla stessa origine (altra acquisizione se da camera)
$( " salva " ).onclick = function () { calcola(ultimo ? ultimo.conFile : false, true); } ;
// #87: il pulsante del file e ' in italiano, l ' input nativo resta nascosto
$( " scegliFile " ).onclick = function () { $( " file " ).click(); } ;
$( " file " ).onchange = function () {
$( " nomeFile " ).textContent = $( " file " ).files[0] ? $( " file " ).files[0].name : " nessuna immagine scelta " ;
};
function bottone(testo, classe, fn) {
var b = document.createElement( " button " ) ;
b.textContent = testo;
b.className = classe || " " ;
b.onclick = fn;
return b ;
}
// #87: «Elimina calibrazione» chiede conferma in pagina con un secondo clic (niente finestre del browser)
var confermaTimer = null ;
function disegnaElenco() {
var t = $( " calibrazioni " ) ;
annullaConferma();
t.textContent = " " ;
C.celle = [];
if (!C.elenco.length) {
var vuota = document.createElement( " div " );
vuota.className = " nota " ;
vuota.textContent = " Nessuna calibrazione salvata. " ;
t.appendChild(vuota);
return;
}
C.elenco.forEach(function (c) {
var sc = document.createElement( " div " ); // #91: scheda della calibrazione
sc.className = " scheda-cal " ;
var attiva = C.attiva !== null && c.nome === C.attiva;
var riga1 = document.createElement( " div " );
riga1.className = " nome-cal " ;
riga1.appendChild(document.createTextNode(c.nome));
if (attiva) {
var s1 = document.createElement( " span " );
s1.className = " segno " ;
s1.textContent = " attiva " ;
riga1.appendChild(s1);
}
var s2 = document.createElement( " span " ); // «risoluzione diversa» dal frame della sorgente (si accende in segnaRisoluzioni)
s2.className = " segno diversa " ;
s2.textContent = " risoluzione diversa " ;
s2.hidden = true;
riga1.appendChild(s2);
C.celle.push( { w: Number(c.image_width), h: Number(c.image_height), span: s2 });
var riga2 = document.createElement( " div " );
riga2.className = " dettagli " ;
riga2.textContent = [
c.image_width && c.image_height ? fmt(c.image_width, 0) + " × " + fmt(c.image_height, 0) : " - " ,
fmt(c.px_per_mm, 3) + " px/mm " ,
c.metodo === " densa " ? " densa " : " planare " ,
c.calibrated_at ? dataItaliana(c.calibrated_at) : " - " ,
descriviOrigine(c.origine)
].join( " · " );
var az = document.createElement( " div " );
az.className = " azioni " ;
var bc = bottone( " Carica " , " " , function () { caricaNome(c.nome); });
bc.disabled = !$( " sorgente " ).value;
az.appendChild(bc);
if (attiva) az.appendChild(bottone( " Scollega " , " secondario " , scollega));
az.appendChild(bottone( " Elimina " , " secondario " , function (e) { eliminaNome(c.nome, e.currentTarget); }));
sc.appendChild(riga1);
sc.appendChild(riga2);
sc.appendChild(az);
t.appendChild(sc);
});
segnaRisoluzioni();
}
// #91: «risoluzione diversa» se il frame noto della sorgente non combacia con quello della calibrazione
function segnaRisoluzioni() {
(C.celle || []).forEach(function (c) {
c.span.hidden = !(A.ris && c.w > 0 && c.h > 0 && (A.ris.w !== c.w || A.ris.h !== c.h));
});
}
// «Carica»: la calibrazione diventa quella attiva della sorgente. Un errore (es. risoluzione diversa) non ferma l ' anteprima.
async function caricaNome(nome) {
var sorgente = $( " sorgente " ).value;
if (!sorgente) { dire( " msgElenco " , " Scegliere una sorgente nella tab Acquisizione. " , " errore " ); return; }
dire( " msgElenco " , " Caricamento di « " + nome + " »... " , " " );
try {
var r = await fetch( " /camera/calibrazione-attiva " , { method: " PUT " , headers: JSON_H, body: JSON.stringify( { source: sorgente, nome: nome }) });
if (sorgente !== $( " sorgente " ).value) return;
if (!r.ok) { dire( " msgElenco " , await dettaglio(r), " errore " ); return; }
dire( " msgElenco " , " Calibrazione « " + nome + " » caricata sulla sorgente. " , " ok " );
aggiornaStato();
aggiornaCalibrata(); // #92: il riquadro calibrata usa subito la nuova calibrazione
} catch (e) {
dire( " msgElenco " , " Servizio non raggiungibile. " , " errore " );
}
}
// «Scollega»: la sorgente torna senza calibrazione, la libreria resta
async function scollega() {
var sorgente = $( " sorgente " ).value;
if (!sorgente) return;
try {
var r = await fetch( " /camera/calibration?source= " + encodeURIComponent(sorgente), { method: " DELETE " });
dire( " msgElenco " , r.ok ? " Calibrazione scollegata dalla sorgente (resta fra le salvate). " : await dettaglio(r), r.ok ? " ok " : " errore " );
} catch (e) {
dire( " msgElenco " , " Servizio non raggiungibile. " , " errore " );
}
aggiornaStato();
aggiornaCalibrata(); // #92: senza calibrazione il riquadro mostra il 409 del servizio
}
// «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser)
function annullaConferma() {
clearTimeout(confermaTimer);
confermaTimer = null;
$( " elimina " ).textContent = " Elimina calibrazione " ;
clearTimeout(C. confermaTimer);
C. confermaTimer = null;
if (C.confermaBtn) C.confermaBtn.textContent = " Elimina " ;
C.confermaBtn = null;
C.confermaNome = null;
}
$( " elimina " ).onclick = async function ( ) {
if (! confermaTi mer ) {
$( " elimina " ).textContent = " Confermi? Clic ancora per eliminare " ;
confermaTi mer = setTimeout(annullaConferma, 4000) ;
async function eliminaNome(nome, btn ) {
if (C. confermaNome !== no me) {
annullaConferma() ;
C. confermaNo me = nome ;
C.confermaBtn = btn;
btn.textContent = " Confermi? Clic ancora " ;
C.confermaTimer = setTimeout(annullaConferma, 4000);
return;
}
annullaConferma();
var sorgente = $( " sorgente " ).value;
try {
var r = await fetch( " /camera/calibrat ion?sourc e= " + encodeURIComponent(sorgent e), { method: " DELETE " });
messaggio( r.ok ? " Calibrazione eliminata. " : await dettaglio(r), r.ok ? " ok " : " errore " );
var r = await fetch( " /camera/calibraz ioni?nom e= " + encodeURIComponent(nom e), { method: " DELETE " });
dire( " msgElenco " , r.ok ? " Calibrazione « " + nome + " » eliminata." : await dettaglio(r), r.ok ? " ok " : " errore " );
} catch (e) {
messaggio( " Servizio non raggiungibile. " , " errore " );
dire( " msgElenco " , " Servizio non raggiungibile. " , " errore " );
}
aggiornaStato();
};
}
$( " sorgente " ).onchange = function () { cambioSorgente(true); };
// #91: il calcolo non salva mai; il servizio tiene il risultato e ne da ' il calcolo_id per «Salva»
async function calcola() {
var sorgente = $( " sorgente " ).value; // #92: sempre la sorgente della tab Acquisizione, frame grezzo
if (!sorgente) { messaggio( " Scegliere una sorgente nella tab Acquisizione. " , " errore " ); return; }
var lato = leggiNumero($( " quadrato " ).value);
if (!(lato > 0)) { messaggio( " Il lato dello scacco deve essere un numero maggiore di zero. " , " errore " ); return; }
var f = new FormData();
f.append( " square_mm " , String(lato)); // al servizio sempre con il punto
f.append( " source " , sorgente);
fermaAnteprima(); // #92: in questa tab il ciclo e ' gia ' sospeso; qui si aspetta anche l ' eventuale richiesta ancora in volo
messaggio( " Calcolo in corso... " , " " );
[ " acquisisci " , " salva " ].forEach(function (i) { $(i).disabled = true; });
try {
while (A.inVolo) await pausa(50);
var r = await fetch( " /camera/calibrate " , { method: " POST " , body: f });
if (!r.ok) { messaggio(await dettaglio(r), " errore " ); return; }
var d = await r.json();
ultimo = { dati: d };
tabella($( " tabella " ), d);
$( " overlay " ).src = " data:image/png;base64, " + d.overlay_png;
$( " avviso " ).textContent = avvisi(d);
$( " origineCalcolo " ).textContent = " Origine: " + descriviOrigine(d.origine);
$( " risultato " ).hidden = false;
annullaSovrascrivi();
dire( " msgSalva " , " " );
messaggio( " Calcolo riuscito: dare un nome e salvare. " , " ok " );
} catch (e) {
messaggio( " Servizio non raggiungibile. " , " errore " );
} finally {
$( " acquisisci " ).disabled = false;
$( " salva " ).disabled = false;
}
}
// «Salva» con nome; se il nome c ' e ' gia ' il pulsante chiede un secondo clic per sovrascrivere
function annullaSovrascrivi() {
clearTimeout(S.timer);
S.timer = null;
S.nome = null;
$( " salva " ).textContent = " Salva " ;
}
async function salvaNome() {
if (!ultimo || !ultimo.dati.calcolo_id) { dire( " msgSalva " , " Nessun calcolo da salvare: calcolare prima. " , " errore " ); return; }
var nome = $( " nomeCal " ).value.trim();
if (!nome) { dire( " msgSalva " , " Scrivere un nome per la calibrazione. " , " errore " ); return; }
if (nome.length > 60) { dire( " msgSalva " , " Il nome può avere al massimo 60 caratteri. " , " errore " ); return; }
var sovrascrivi = S.timer !== null && S.nome === nome.toLowerCase(); // secondo clic sullo stesso nome
annullaSovrascrivi();
$( " salva " ).disabled = true;
try {
var r = await fetch( " /camera/calibrazioni " , {
method: " POST " , headers: JSON_H,
body: JSON.stringify( { calcolo_id: ultimo.dati.calcolo_id, nome: nome, sovrascrivi: sovrascrivi })
});
if (r.status === 409) {
S.nome = nome.toLowerCase();
$( " salva " ).textContent = " Sovrascrivi « " + nome + " »? Clic ancora " ;
S.timer = setTimeout(annullaSovrascrivi, 4000);
dire( " msgSalva " , await dettaglio(r), " errore " );
return;
}
if (!r.ok) { dire( " msgSalva " , await dettaglio(r), " errore " ); return; }
dire( " msgSalva " , " Calibrazione « " + nome + " » salvata. Per usarla su una sorgente: «Carica» nell ' elenco. " , " ok " );
caricaCalibrazioni();
} catch (e) {
dire( " msgSalva " , " Servizio non raggiungibile. " , " errore " );
} finally {
$( " salva " ).disabled = false;
}
}
$( " acquisisci " ).onclick = calcola;
$( " salva " ).onclick = salvaNome;
$( " nomeCal " ).onkeydown = function (e) { if (e.key === " Enter " ) salvaNome(); };
$( " sorgente " ).onchange = function () { dire( " msgSorgente " , " " ); cambioSorgente(true); }; // #92
scegliTab(memoria( " tmf.acq.tab " ) === " cal " ? " cal " : " acq " , false); // #91: tab ricordata
caricaSorgenti();
</script>
</body>