Compare commits

...

5 Commits

Author SHA1 Message Date
Adriano f504c86491 Acquisizione: immagine da file come sorgente, riquadri grezza e calibrata affiancati (Refs #92)
Pagina: Sorgente in cima alla tab Acquisizione con carica/rimuovi immagine,
riquadro Calibrata con casella, calibrazione sempre dal frame grezzo della
sorgente, ciclo anteprima sospeso nella tab Calibrazione.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 16:27:20 +02:00
Adriano 8eb80e20a8 Acquisizione: elenco calibrazioni a schede, parametri in lettura (Refs #91)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 16:14:37 +02:00
Adriano b2f82fe779 merge #91 pagina: tab Acquisizione / Calibrazione 2026-10-24 16:10:25 +02:00
Adriano 3c5c5f9bc5 merge #91 servizio: calibrazioni con nome, archivio unico 2026-10-24 16:10:25 +02:00
Adriano d6cee498ad Acquisizione: pagina a due tab, calibrazioni salvate con nome e carica per sorgente (Refs #91)
Tab Acquisizione | Calibrazione con anteprima sempre visibile; il calcolo non salva piu',
Salva usa calcolo_id e un nome (conferma di sovrascrittura al secondo clic);
elenco unico con Carica / Scollega / Elimina e segno di risoluzione diversa.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 16:08:02 +02:00
3 changed files with 706 additions and 204 deletions
+587 -199
View File
@@ -8,6 +8,15 @@ Parla con le rotte dello stesso servizio: /camera/sources, /camera/anteprima,
/camera/parametri, /camera/parametri/salva, /camera/calibration, /camera/calibrate. /camera/parametri, /camera/parametri/salva, /camera/calibration, /camera/calibrate.
#84: calibrazione densa, un solo dato dell'operatore (lato dello scacco in mm). #84: calibrazione densa, un solo dato dell'operatore (lato dello scacco in mm).
#87: anteprima, parametri camera, stile TMF. #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. 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; } 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 { 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; } .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, background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; } 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; } .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 { background: var(--sfondo); border: 1px solid var(--bordo); border-radius: 6px; padding: 8px 10px; }
.indicatore .nome { font-size: .78rem; color: var(--secondario); } .indicatore .nome { font-size: .78rem; color: var(--secondario); }
@@ -75,121 +93,166 @@ PAGINA = r"""<!doctype html>
.parametro .campo input[type=text] { width: 110px; text-align: right; flex: none; } .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 .unita { font-size: .82rem; color: var(--secondario); min-width: 2.2em; }
.parametro .limiti { font-size: .75rem; color: var(--secondario); } .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) { @media (max-width: 900px) {
.griglia { grid-template-columns: minmax(0, 1fr); } .griglia { grid-template-columns: minmax(0, 1fr); }
.riquadri.doppio { grid-template-columns: minmax(0, 1fr); } /* #92: in colonna */
.cornice img { max-height: 60vh; } .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> </style>
</head> </head>
<body> <body>
<main> <main>
<h1>Acquisizione camera</h1> <h1>Acquisizione camera</h1>
<div class="griglia"> <!-- #91: due tab; #92: l'anteprima sta nella tab Acquisizione e si sospende nella tab Calibrazione -->
<div class="colonna"> <div class="tabs" role="tablist" aria-label="Sezioni della pagina">
<!-- #87: sezione Anteprima --> <button id="tabAcq" type="button" role="tab" aria-selected="true" aria-controls="pannelloAcq">Acquisizione</button>
<section class="scheda" id="sezioneAnteprima"> <button id="tabCal" type="button" role="tab" aria-selected="false" aria-controls="pannelloCal" tabindex="-1">Calibrazione</button>
<h2>Anteprima</h2> </div>
<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="mirinoCasella" type="checkbox"> Mirino centrale</label>
</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>
</div>
</div>
<div class="indicatori">
<div class="indicatore">
<div class="nome">Nitidezza</div>
<div class="valore" id="nitidezza">-</div>
<div class="barra"><div id="barraNitidezza"></div></div>
<div class="piccolo">massimo visto: <span id="nitidezzaMax">-</span>
<button id="azzeraMax" class="secondario" style="padding:1px 7px;font-size:.75rem;margin-left:6px">Azzera</button></div>
</div>
<div class="indicatore">
<div class="nome">Saturati</div>
<div class="valore" id="saturati">-</div>
<div class="piccolo">pixel a 250 o più; da tenere sotto l'1 %</div>
</div>
<div class="indicatore">
<div class="nome">Livello medio</div>
<div class="valore" id="media">-</div>
<div class="piccolo">scala 0-255</div>
</div>
<div class="indicatore">
<div class="nome">Frame pieno</div>
<div class="valore" id="dimensioni">-</div>
<div class="piccolo" id="raddrizzato">&nbsp;</div>
</div>
</div>
<div id="msgAnteprima" class="messaggio"></div>
</section>
</div>
<div class="colonna"> <div class="colonna" id="pannelloAcq" role="tabpanel" aria-labelledby="tabAcq">
<!-- #87: sezione Sorgente --> <!-- #92: Sorgente in cima, a larghezza piena: camere, cartella e immagini caricate da file -->
<section class="scheda" id="sezioneSorgente"> <section class="scheda" id="sezioneSorgente">
<h2>Sorgente</h2> <h2>Sorgente</h2>
<label for="sorgente">Camera o cartella</label> <div class="sorgente-riga">
<select id="sorgente"></select> <div class="campo-sorgente">
<div class="stato" id="statoSorgente">-</div> <label for="sorgente">Camera o immagine</label>
<div id="msgSorgente" class="messaggio"></div> <select id="sorgente"></select>
</section>
<!-- #87: sezione Parametri camera -->
<section class="scheda" id="sezioneParametri">
<h2>Parametri camera</h2>
<div id="parametri"><div class="nota">-</div></div>
<div class="riga" id="salvaParametriRiga" hidden>
<button id="salvaParametri" class="secondario">Salva come predefiniti</button>
</div> </div>
<div id="msgParametri" class="messaggio"></div> <button id="caricaImmagine" class="secondario" type="button">Carica immagine da file...</button>
</section> <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>
<section class="scheda" id="sezioneCalibrazione"> <!-- #87: sezione Anteprima; #92: barra, due riquadri affiancati (grezza | calibrata), indicatori dal frame grezzo -->
<h2>Calibrazione</h2> <section class="scheda" id="sezioneAnteprima">
<div class="riga"> <h2>Anteprima</h2>
<div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div> <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="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 grezza della sorgente">
<div id="mirino" class="mirino" hidden></div>
</div>
</div>
</div> </div>
<p class="nota"> <div class="riquadro" id="riquadroCal" hidden>
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco. <div class="titolo">Calibrata<span id="nomeCalibrata"></span></div>
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica. <div class="palco">
Durante il calcolo l'anteprima si sospende.</p> <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">
<div class="indicatore">
<div class="nome">Nitidezza</div>
<div class="valore" id="nitidezza">-</div>
<div class="barra"><div id="barraNitidezza"></div></div>
<div class="piccolo">massimo visto: <span id="nitidezzaMax">-</span>
<button id="azzeraMax" class="secondario" style="padding:1px 7px;font-size:.75rem;margin-left:6px">Azzera</button></div>
</div>
<div class="indicatore">
<div class="nome">Saturati</div>
<div class="valore" id="saturati">-</div>
<div class="piccolo">pixel a 250 o più; da tenere sotto l'1 %</div>
</div>
<div class="indicatore">
<div class="nome">Livello medio</div>
<div class="valore" id="media">-</div>
<div class="piccolo">scala 0-255</div>
</div>
<div class="indicatore">
<div class="nome">Frame pieno</div>
<div class="valore" id="dimensioni">-</div>
<div class="piccolo" id="raddrizzato">&nbsp;</div>
</div>
</div>
<div id="msgAnteprima" class="messaggio"></div>
</section>
<!-- #87: sezione Parametri camera -->
<section class="scheda" id="sezioneParametri">
<h2>Parametri camera</h2>
<div id="parametri"><div class="nota">-</div></div>
<div class="riga" id="salvaParametriRiga" hidden>
<button id="salvaParametri" class="secondario">Salva come predefiniti</button>
</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="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.
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 class="riga" style="margin-top:10px">
<button id="acquisisci">Acquisisci e calibra</button> <div><label for="nomeCal">Nome</label><input id="nomeCal" type="text" maxlength="60" size="22" autocomplete="off"></div>
<button id="scegliFile" class="secondario">Scegli immagine...</button> <button id="salva">Salva</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> </div>
<div id="messaggio" class="messaggio"></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>
</section>
<div id="risultato" hidden style="margin-top:10px"> <section class="scheda" id="sezioneSalvate">
<strong>Risultato del calcolo</strong> <h2>Calibrazioni salvate</h2>
<table id="tabella"></table> <div id="stato" class="stato">-</div>
<div id="avviso" class="avviso"></div> <div id="calibrazioni" class="elenco"></div> <!-- #91: una scheda per calibrazione, niente tabella larga -->
<div class="riga" style="margin-top:10px"> <div id="msgElenco" class="messaggio"></div>
<button id="salva">Salva</button> </section>
<span class="nota" style="margin:0 0 6px">Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.</span>
</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>
</div> </div>
</main> </main>
@@ -248,8 +311,11 @@ async function dettaglio(risposta) {
// ---- Sorgente // ---- 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 sel = $("sorgente");
var prima = sel.value;
sel.textContent = "";
try { try {
var r = await fetch("/camera/sources"); var r = await fetch("/camera/sources");
var j = await r.json(); var j = await r.json();
@@ -257,24 +323,86 @@ async function caricaSorgenti() {
var o = document.createElement("option"); var o = document.createElement("option");
o.value = s.name; o.value = s.name;
o.textContent = s.label || s.name; o.textContent = s.label || s.name;
o.dataset.kind = s.kind || ""; // #92: «image» = immagine caricata da file
sel.appendChild(o); sel.appendChild(o);
}); });
} catch (e) { dire("msgSorgente", "Elenco delle sorgenti non disponibile.", "errore"); } } catch (e) { dire("msgSorgente", "Elenco delle sorgenti non disponibile.", "errore"); }
if (!sel.options.length) dire("msgSorgente", "Nessuna sorgente camera trovata.", "errore"); if (!sel.options.length) dire("msgSorgente", "Nessuna sorgente camera trovata.", "errore");
var ricordata = memoria("tmf.acq.sorgente"); // #87: ultima scelta ricordata var voluta = scegli || (prima === "" ? memoria("tmf.acq.sorgente") : prima); // #87: ultima scelta ricordata
if (ricordata) { if (voluta) {
for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === ricordata) sel.value = ricordata; for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === voluta) sel.value = voluta;
} }
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 // #87: cambiando sorgente si azzerano gli indicatori e si rileggono parametri e calibrazione
function cambioSorgente(salva) { function cambioSorgente(salva) {
if (salva) memoria("tmf.acq.sorgente", $("sorgente").value); if (salva) memoria("tmf.acq.sorgente", $("sorgente").value);
var era = A.attiva; var era = A.attiva;
fermaAnteprima(); fermaAnteprima();
azzeraIndicatori(); azzeraIndicatori();
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
annullaRimuovi();
$("rimuoviImmagine").hidden = !sorgenteImmagine();
dire("msgAnteprima", ""); dire("msgAnteprima", "");
dire("msgElenco", ""); // #91
aggiornaStato(); aggiornaStato();
caricaParametri(); caricaParametri();
if (era) avviaAnteprima(); if (era) avviaAnteprima();
@@ -282,7 +410,8 @@ function cambioSorgente(salva) {
// ---- Anteprima (#87) // ---- Anteprima (#87)
var A = { attiva: false, gen: 0, inVolo: false, url: null, nitMax: 0 }; // #91: ris = {w, h} dell'ultimo frame; #92: urlCal/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() { function azzeraIndicatori() {
A.nitMax = 0; A.nitMax = 0;
@@ -290,6 +419,8 @@ function azzeraIndicatori() {
$("saturati").className = "valore"; $("saturati").className = "valore";
$("barraNitidezza").style.width = "0"; $("barraNitidezza").style.width = "0";
$("raddrizzato").textContent = " "; $("raddrizzato").textContent = " ";
A.ris = null; // #91: risoluzione del frame non piu' nota
segnaRisoluzioni();
} }
function pulsantiAnteprima() { function pulsantiAnteprima() {
@@ -306,6 +437,52 @@ function mostraImmagine(blob) {
$("vuoto").hidden = true; $("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) { function aggiornaIndicatori(h) {
var nit = parseFloat(h.get("X-Nitidezza")); var nit = parseFloat(h.get("X-Nitidezza"));
var sat = parseFloat(h.get("X-Saturati-Pct")); var sat = parseFloat(h.get("X-Saturati-Pct"));
@@ -322,9 +499,13 @@ function aggiornaIndicatori(h) {
} }
if (isFinite(media)) $("media").textContent = fmt(media, 1); if (isFinite(media)) $("media").textContent = fmt(media, 1);
var w = h.get("X-Larghezza"), a = h.get("X-Altezza"); var w = h.get("X-Larghezza"), a = h.get("X-Altezza");
if (w && a) $("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0); if (w && a) {
var r = h.get("X-Raddrizzato"); $("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0);
$("raddrizzato").textContent = r === "si" ? "raddrizzata dalla calibrazione" : r === "no" ? "non raddrizzata" : " "; // #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) // 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; var sorgente = $("sorgente").value;
if (!sorgente) { dire("msgAnteprima", "Scegliere una sorgente.", "errore"); return "fatale"; } if (!sorgente) { dire("msgAnteprima", "Scegliere una sorgente.", "errore"); return "fatale"; }
var g = A.gen; var g = A.gen;
A.lato = lato;
A.inVolo = true; A.inVolo = true;
try { try {
var r = await fetch("/camera/anteprima", { var r = await fetch("/camera/anteprima", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, 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) { if (!r.ok) {
var d = await dettaglio(r); var d = await dettaglio(r);
@@ -349,6 +531,7 @@ async function unaAnteprima(lato) {
mostraImmagine(blob); mostraImmagine(blob);
aggiornaIndicatori(r.headers); aggiornaIndicatori(r.headers);
dire("msgAnteprima", ""); dire("msgAnteprima", "");
await aggiornaCalibrata(lato); // #92: stesso frame, raddrizzato; resta in volo finche' non risponde
return "ok"; return "ok";
} catch (e) { } catch (e) {
if (g === A.gen) dire("msgAnteprima", "Servizio non raggiungibile.", "errore"); if (g === A.gen) dire("msgAnteprima", "Servizio non raggiungibile.", "errore");
@@ -398,7 +581,15 @@ $("avvia").onclick = avviaAnteprima;
$("ferma").onclick = fermaAnteprima; $("ferma").onclick = fermaAnteprima;
$("scatta").onclick = scatta; $("scatta").onclick = scatta;
$("azzeraMax").onclick = function () { A.nitMax = 0; $("nitidezzaMax").textContent = "-"; $("barraNitidezza").style.width = "0"; }; $("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) // ---- Parametri camera (#87)
@@ -439,6 +630,12 @@ async function caricaParametri() {
$("salvaParametriRiga").hidden = true; $("salvaParametriRiga").hidden = true;
dire("msgParametri", ""); dire("msgParametri", "");
if (!sorgente) { $("parametri").textContent = "-"; return; } 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 { try {
var r = await fetch("/camera/parametri?source=" + encodeURIComponent(sorgente)); var r = await fetch("/camera/parametri?source=" + encodeURIComponent(sorgente));
if (g !== P.gen) return; 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) // ---- Calibrazione (logica di #83 e #84; #87: lato precompilato, data italiana, conferma in pagina)
function righe(d) { 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 // avvisi sulla qualita' (solo calibrazione densa): copertura bassa o residuo alto
function avvisi(d) { function avvisi(d) {
var a = []; var a = [];
@@ -687,78 +880,273 @@ function avvisi(d) {
return a.join(" "); 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; var sorgente = $("sorgente").value;
if (!sorgente) { messaggio("Scegliere una sorgente.", "errore"); return; } var stato = $("stato");
var lato = leggiNumero($("quadrato").value); annullaConferma();
if (!(lato > 0)) { messaggio("Il lato dello scacco deve essere un numero maggiore di zero.", "errore"); return; } $("calSorgente").textContent = sorgente ? $("sorgente").options[$("sorgente").selectedIndex].textContent : "-"; // #91
var f = new FormData(); if (!sorgente) { stato.textContent = "-"; $("statoSorgente").textContent = "-"; caricaCalibrazioni(); return; }
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; });
try { try {
var r = await fetch("/camera/calibrate", { method: "POST", body: f }); var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente));
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; } if (sorgente !== $("sorgente").value) return; // nel frattempo si e' cambiata sorgente
var d = await r.json(); if (r.status === 404) {
ultimo = { conFile: conFile, dati: d }; stato.textContent = "Sorgente non calibrata.";
tabella($("tabella"), d); $("statoSorgente").textContent = "non calibrata";
$("overlay").src = "data:image/png;base64," + d.overlay_png; } else if (!r.ok) {
$("avviso").textContent = avvisi(d); stato.textContent = await dettaglio(r);
$("risultato").hidden = false; } else {
messaggio(d.saved ? "Calibrazione salvata." : "Calibrazione riuscita: controllare e salvare.", "ok"); var d = await r.json();
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) { } catch (e) {
messaggio("Servizio non raggiungibile.", "errore"); stato.textContent = "Servizio non raggiungibile.";
} finally { }
["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = false; }); caricaCalibrazioni();
if (eraAttiva) avviaAnteprima(); }
// #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); }; function bottone(testo, classe, fn) {
$("carica").onclick = function () { calcola(true, false); }; var b = document.createElement("button");
// «Salva» rifa il calcolo con save=true sulla stessa origine (altra acquisizione se da camera) b.textContent = testo;
$("salva").onclick = function () { calcola(ultimo ? ultimo.conFile : false, true); }; b.className = classe || "";
// #87: il pulsante del file e' in italiano, l'input nativo resta nascosto b.onclick = fn;
$("scegliFile").onclick = function () { $("file").click(); }; return b;
$("file").onchange = function () {
$("nomeFile").textContent = $("file").files[0] ? $("file").files[0].name : "nessuna immagine scelta";
};
// #87: «Elimina calibrazione» chiede conferma in pagina con un secondo clic (niente finestre del browser)
var confermaTimer = null;
function annullaConferma() {
clearTimeout(confermaTimer);
confermaTimer = null;
$("elimina").textContent = "Elimina calibrazione";
} }
$("elimina").onclick = async function () {
if (!confermaTimer) { function disegnaElenco() {
$("elimina").textContent = "Confermi? Clic ancora per eliminare"; var t = $("calibrazioni");
confermaTimer = setTimeout(annullaConferma, 4000); 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(C.confermaTimer);
C.confermaTimer = null;
if (C.confermaBtn) C.confermaBtn.textContent = "Elimina";
C.confermaBtn = null;
C.confermaNome = null;
}
async function eliminaNome(nome, btn) {
if (C.confermaNome !== nome) {
annullaConferma();
C.confermaNome = nome;
C.confermaBtn = btn;
btn.textContent = "Confermi? Clic ancora";
C.confermaTimer = setTimeout(annullaConferma, 4000);
return; return;
} }
annullaConferma(); annullaConferma();
var sorgente = $("sorgente").value;
try { try {
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" }); var r = await fetch("/camera/calibrazioni?nome=" + encodeURIComponent(nome), { method: "DELETE" });
messaggio(r.ok ? "Calibrazione eliminata." : await dettaglio(r), r.ok ? "ok" : "errore"); dire("msgElenco", r.ok ? "Calibrazione «" + nome + "» eliminata." : await dettaglio(r), r.ok ? "ok" : "errore");
} catch (e) { } catch (e) {
messaggio("Servizio non raggiungibile.", "errore"); dire("msgElenco", "Servizio non raggiungibile.", "errore");
} }
aggiornaStato(); 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(); caricaSorgenti();
</script> </script>
</body> </body>
+5 -5
View File
@@ -29,10 +29,10 @@ def test_offline_e_senza_finestre_del_browser():
def test_id_principali(): def test_id_principali():
ids = set(re.findall(r'id="([^"]+)"', PAGINA)) ids = set(re.findall(r'id="([^"]+)"', PAGINA))
attesi = {"sorgente", "avvia", "ferma", "scatta", "grezzo", "mirinoCasella", "immagine", attesi = {"sorgente", "avvia", "ferma", "scatta", "mirinoCasella", "immagine",
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni", "nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
"parametri", "salvaParametri", "quadrato", "acquisisci", "scegliFile", "file", "parametri", "salvaParametri", "quadrato", "acquisisci", "caricaImmagine", "fileImmagine",
"carica", "salva", "elimina", "stato", "statoSorgente"} "mostraCal", "salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga; #92: file e calibrata nell'Acquisizione
assert attesi <= ids, attesi - ids assert attesi <= ids, attesi - ids
# ogni id usato dal JS con $("...") esiste nel markup # ogni id usato dal JS con $("...") esiste nel markup
usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA)) usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
@@ -41,8 +41,8 @@ def test_id_principali():
def test_sezioni_e_testi_italiani(): def test_sezioni_e_testi_italiani():
for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione", for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione",
"Salva come predefiniti", "Scegli immagine...", "Questa sorgente non ha parametri regolabili", "Salva come predefiniti", "Carica immagine da file...", "Questa sorgente non ha parametri regolabili",
"Immagine grezza (senza correzione)"): "Mostra immagine calibrata"): # #92
assert testo in PAGINA, testo assert testo in PAGINA, testo
assert "it-IT" in PAGINA # numeri con la virgola assert "it-IT" in PAGINA # numeri con la virgola
assert "Choose File" not in PAGINA assert "Choose File" not in PAGINA
+114
View File
@@ -0,0 +1,114 @@
"""Test della pagina a due tab (#91, #92): contenuto statico di PAGINA, senza servizio."""
import importlib.util
import re
from pathlib import Path
_FILE = Path(__file__).resolve().parents[1] / "calibrazione_pagina.py"
_spec = importlib.util.spec_from_file_location("calibrazione_pagina_tab", _FILE)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
PAGINA = _mod.PAGINA
def test_due_tab_con_ruolo_e_selezione():
assert re.search(r'<button id="tabAcq"[^>]*role="tab"[^>]*aria-selected="true"', PAGINA)
assert re.search(r'<button id="tabCal"[^>]*role="tab"[^>]*aria-selected="false"', PAGINA)
assert ">Acquisizione</button>" in PAGINA and ">Calibrazione</button>" in PAGINA
assert 'role="tablist"' in PAGINA and 'role="tabpanel"' in PAGINA
assert 'aria-selected", cal ?' in PAGINA # la selezione cambia da JS
assert '"tmf.acq.tab"' in PAGINA # tab scelta ricordata (via memoria(), protetta)
def test_sorgente_in_cima_poi_riquadri():
# #92: Sorgente, anteprima e parametri stanno nel pannello Acquisizione; Sorgente prima dei riquadri
i_acq = PAGINA.index('id="pannelloAcq"')
i_cal = PAGINA.index('id="pannelloCal"')
i_sorg = PAGINA.index('id="sezioneSorgente"')
i_ant = PAGINA.index('id="sezioneAnteprima"')
i_riq = PAGINA.index('id="riquadri"')
assert i_acq < i_sorg < i_ant < i_riq < PAGINA.index('id="sezioneParametri"') < i_cal
assert PAGINA.index('id="sezioneCalibrazione"') > i_cal
# barra: casella calibrata e mirino; la vecchia casella «Immagine grezza» non c'e' piu'
assert 'id="mostraCal"' in PAGINA and "Mostra immagine calibrata" in PAGINA
assert 'id="grezzo"' not in PAGINA and "Immagine grezza (senza correzione)" not in PAGINA
# riquadri: «Immagine» sempre visibile, «Calibrata» nascosta finche' la casella e' spenta
assert re.search(r'<div class="riquadro" id="riquadroCal" hidden>', PAGINA)
assert "grezzo: true" in PAGINA
def test_id_del_pannello_calibrazione():
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
attesi = {"calSorgente", "quadrato", "acquisisci", "nomeCal", "salva", "msgSalva", "calibrazioni",
"msgElenco", "stato", "risultato", "overlay",
"caricaImmagine", "fileImmagine", "rimuoviImmagine", "riquadroCal", "immagineCal", "mirinoCal"}
assert attesi <= ids, attesi - ids
usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
assert usati <= ids, usati - ids
assert len(re.findall(r'id="([^"]+)"', PAGINA)) == len(ids) # nessun id doppio
def test_calibrazione_senza_file_ne_radio():
# #92: l'immagine da file e' una sorgente di acquisizione, non un'opzione della calibrazione
for sparito in ("origFile", "origSorgente", "scegliFile", "nomeFile", 'type="radio"', "Immagine da file", "Sorgente selezionata"):
assert sparito not in PAGINA, sparito
# l'unico input file e' nella sezione Sorgente, in cima alla tab Acquisizione
assert PAGINA.count('type="file"') == 1
assert PAGINA.index('type="file"') < PAGINA.index('id="pannelloCal"')
assert "immagine grezza dalla tab Acquisizione" in PAGINA
# il calcolo manda solo la sorgente (e il lato), mai il file
assert 'f.append("source", sorgente)' in PAGINA and 'f.append("image"' not in PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
def test_testi_del_pannello():
for testo in ("Camera o immagine", "Carica immagine da file...", "Rimuovi immagine", "Calcola",
"Calibrazioni salvate", "risoluzione diversa", "Calibrata",
"Carica", "Elimina", "Scollega", "Sovrascrivi «", "Clic ancora", "Nome",
"calibrata: ", "non calibrata"):
assert testo in PAGINA, testo
def test_rotte_del_contratto():
for rotta in ("/camera/calibrazioni", "/camera/calibrazione-attiva", "/camera/calibrate",
"/camera/calibration?source=", "/camera/immagini", "/camera/immagini?nome=",
"/camera/anteprima/calibrata"): # #92
assert '"' + rotta in PAGINA, rotta
for parola in ("calcolo_id", "sovrascrivi", "method: \"PUT\"", "method: \"DELETE\"", "X-Larghezza", "X-Altezza"):
assert parola in PAGINA, parola
# il calcolo non salva piu': il campo save sparisce
assert 'append("save"' not in PAGINA
def test_nessuna_finestra_del_browser_e_offline():
for vietato in ("alert(", "confirm(", "prompt(", "http://", "https://"):
assert vietato not in PAGINA, vietato
def test_acquisizione_di_87_90_conservata():
# anteprima, indicatori, mirino, grezzo, parametri camera e ciclo a 100 ms restano
for parola in ("Parametri camera", "Mirino centrale",
"Math.max(100 - (Date.now() - t0), 0)", "/camera/anteprima", "/camera/parametri"):
assert parola in PAGINA, parola
def test_pagina_stretta():
assert "@media (max-width: 500px)" in PAGINA
# #91: elenco a schede, niente tabella larga ne' scorrimento orizzontale
assert 'id="calibrazioni" class="elenco"' in PAGINA and "overflow-x" not in PAGINA
assert "Lettura dei parametri..." in PAGINA
def test_ciclo_sospeso_nella_tab_calibrazione():
# #92: entrando in Calibrazione il ciclo si ferma e poi riprende; Calcola aspetta le richieste in volo
assert "A.riprendi = true; fermaAnteprima();" in PAGINA
assert "if (!cal && A.riprendi)" in PAGINA
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
assert "fermaAnteprima();" in calcola and "while (A.inVolo)" in calcola
def test_calibrata_dopo_anteprima_e_dopo_carica():
# #92: la calibrata segue ogni anteprima grezza riuscita e si aggiorna dopo Carica/Scollega; 404/409 al posto dell'immagine
assert "await aggiornaCalibrata(lato);" in PAGINA
assert PAGINA.count("aggiornaCalibrata();") >= 3
assert "messaggioCalibrata(await dettaglio(r))" in PAGINA
assert 'decodeURIComponent(r.headers.get("X-Calibrazione")' in PAGINA