merge #92 pagina: sorgente in cima, grezza e calibrata affiancate

This commit is contained in:
2026-10-24 16:28:00 +02:00
3 changed files with 332 additions and 164 deletions
+276 -145
View File
@@ -12,6 +12,11 @@ Parla con le rotte dello stesso servizio: /camera/sources, /camera/anteprima,
visibile in entrambe. Calibrazioni salvate con nome in un archivio unico visibile in entrambe. Calibrazioni salvate con nome in un archivio unico
(/camera/calibrazioni, /camera/calibrazione-attiva); il calcolo non salva piu': (/camera/calibrazioni, /camera/calibrazione-attiva); il calcolo non salva piu':
si salva dal calcolo_id con un nome. 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.
""" """
@@ -61,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); }
@@ -94,10 +108,9 @@ PAGINA = r"""<!doctype html>
.scheda-cal .azioni button { padding: 3px 10px; font-size: .82rem; } .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 { 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; } .segno.diversa { background: var(--avviso); color: #422006; }
.origine label { display: inline-block; margin: 0 14px 4px 0; color: var(--testo); font-size: .92rem; }
input[type=radio] { accent-color: var(--accento); }
@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) */ @media (max-width: 500px) { /* #91: utilizzabile anche stretta (400 px) */
@@ -112,125 +125,134 @@ PAGINA = r"""<!doctype html>
<main> <main>
<h1>Acquisizione camera</h1> <h1>Acquisizione camera</h1>
<!-- #91: due tab; la colonna dell'anteprima resta visibile in entrambe --> <!-- #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"> <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="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> <button id="tabCal" type="button" role="tab" aria-selected="false" aria-controls="pannelloCal" tabindex="-1">Calibrazione</button>
</div> </div>
<div class="griglia"> <div class="colonna" id="pannelloAcq" role="tabpanel" aria-labelledby="tabAcq">
<div class="colonna"> <!-- #92: Sorgente in cima, a larghezza piena: camere, cartella e immagini caricate da file -->
<!-- #87: sezione Anteprima --> <section class="scheda" id="sezioneSorgente">
<section class="scheda" id="sezioneAnteprima"> <h2>Sorgente</h2>
<h2>Anteprima</h2> <div class="sorgente-riga">
<div class="riga"> <div class="campo-sorgente">
<button id="avvia">Avvia anteprima</button> <label for="sorgente">Camera o immagine</label>
<button id="ferma" class="secondario" disabled>Ferma anteprima</button> <select id="sorgente"></select>
<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>
<div class="palco"> <button id="caricaImmagine" class="secondario" type="button">Carica immagine da file...</button>
<div id="vuoto">Anteprima ferma. Premere «Avvia anteprima» o «Scatta».</div> <input id="fileImmagine" type="file" accept="image/*" hidden>
<div class="cornice" id="cornice" hidden> <button id="rimuoviImmagine" class="secondario" type="button" hidden>Rimuovi immagine</button>
<img id="immagine" alt="Anteprima della camera"> </div>
<div id="mirino" hidden></div> <div class="stato" id="statoSorgente">-</div>
</div> <div id="msgSorgente" class="messaggio"></div>
</div> </section>
<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" id="pannelloAcq" role="tabpanel" aria-labelledby="tabAcq"> <!-- #87: sezione Anteprima; #92: barra, due riquadri affiancati (grezza | calibrata), indicatori dal frame grezzo -->
<!-- #87: sezione Sorgente --> <section class="scheda" id="sezioneAnteprima">
<section class="scheda" id="sezioneSorgente"> <h2>Anteprima</h2>
<h2>Sorgente</h2> <div class="riga">
<label for="sorgente">Camera o cartella</label> <button id="avvia">Avvia anteprima</button>
<select id="sorgente"></select> <button id="ferma" class="secondario" disabled>Ferma anteprima</button>
<div class="stato" id="statoSorgente">-</div> <button id="scatta" class="secondario">Scatta</button>
<div id="msgSorgente" class="messaggio"></div> <label style="margin:0 0 6px"><input id="mostraCal" type="checkbox"> Mostra immagine calibrata</label>
</section> <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 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">
<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 --> <!-- #87: sezione Parametri camera -->
<section class="scheda" id="sezioneParametri"> <section class="scheda" id="sezioneParametri">
<h2>Parametri camera</h2> <h2>Parametri camera</h2>
<div id="parametri"><div class="nota">-</div></div> <div id="parametri"><div class="nota">-</div></div>
<div class="riga" id="salvaParametriRiga" hidden> <div class="riga" id="salvaParametriRiga" hidden>
<button id="salvaParametri" class="secondario">Salva come predefiniti</button> <button id="salvaParametri" class="secondario">Salva come predefiniti</button>
</div> </div>
<div id="msgParametri" class="messaggio"></div> <div id="msgParametri" class="messaggio"></div>
</section> </section>
</div> </div>
<!-- #91: tab Calibrazione (colonna destra al posto di Sorgente e Parametri) --> <!-- #91: tab Calibrazione; #92: sempre sul frame grezzo della sorgente scelta in Acquisizione, niente file qui -->
<div class="colonna" id="pannelloCal" role="tabpanel" aria-labelledby="tabCal" hidden> <div class="griglia" id="pannelloCal" role="tabpanel" aria-labelledby="tabCal" hidden>
<section class="scheda" id="sezioneCalibrazione"> <section class="scheda" id="sezioneCalibrazione">
<h2>Calibrazione</h2> <h2>Calibrazione</h2>
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> (si cambia nella tab Acquisizione)</div> <div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
<div class="origine"> <div class="riga" style="margin-top:10px">
<label for="origSorgente"><input id="origSorgente" name="origine" type="radio" value="sorgente" checked> Sorgente selezionata</label> <div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div>
<label for="origFile"><input id="origFile" name="origine" type="radio" value="file"> Immagine da file</label> <button id="acquisisci" title="Acquisisci e calibra">Calcola</button>
</div> </div>
<div class="riga" style="margin-top:4px"> <p class="nota">
<button id="scegliFile" class="secondario">Scegli immagine...</button> Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
<input id="file" type="file" accept="image/*" hidden> La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
<span id="nomeFile" class="nota" style="margin:0 0 6px">nessuna immagine scelta</span> Il frame è sempre quello grezzo della sorgente; l'anteprima resta sospesa finché questa tab è aperta.</p>
</div> <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">
<div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div> <div><label for="nomeCal">Nome</label><input id="nomeCal" type="text" maxlength="60" size="22" autocomplete="off"></div>
<button id="acquisisci" title="Acquisisci e calibra">Calcola</button> <button id="salva">Salva</button>
</div> </div>
<p class="nota"> <div id="msgSalva" class="messaggio"></div>
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco. <img id="overlay" alt="Angoli trovati sulla scacchiera" style="max-width:100%;margin-top:8px;border:1px solid var(--bordo);border-radius:6px">
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica. </div>
Calcolando dalla sorgente l'anteprima si sospende e poi riprende.</p> </section>
<div id="messaggio" class="messaggio"></div>
<div id="risultato" hidden style="margin-top:10px"> <section class="scheda" id="sezioneSalvate">
<strong>Risultato del calcolo</strong> <h2>Calibrazioni salvate</h2>
<div id="origineCalcolo" class="nota" style="margin:2px 0 6px"></div> <div id="stato" class="stato">-</div>
<table id="tabella"></table> <div id="calibrazioni" class="elenco"></div> <!-- #91: una scheda per calibrazione, niente tabella larga -->
<div id="avviso" class="avviso"></div> <div id="msgElenco" class="messaggio"></div>
<div class="riga" style="margin-top:10px"> </section>
<div><label for="nomeCal">Nome</label><input id="nomeCal" type="text" maxlength="60" size="22" autocomplete="off"></div>
<button id="salva">Salva</button>
</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>
<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>
</div> </div>
</main> </main>
@@ -289,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();
@@ -298,23 +323,84 @@ 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 dire("msgElenco", ""); // #91
aggiornaStato(); aggiornaStato();
@@ -324,7 +410,8 @@ function cambioSorgente(salva) {
// ---- Anteprima (#87) // ---- Anteprima (#87)
var A = { attiva: false, gen: 0, inVolo: false, url: null, nitMax: 0, ris: null }; // #91: ris = {w, h} dell'ultimo frame // #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;
@@ -350,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"));
@@ -372,8 +505,7 @@ function aggiornaIndicatori(h) {
var nw = parseInt(w, 10), na = parseInt(a, 10); 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(); } if (!A.ris || A.ris.w !== nw || A.ris.h !== na) { A.ris = { w: nw, h: na }; segnaRisoluzioni(); }
} }
var r = h.get("X-Raddrizzato"); $("raddrizzato").textContent = "frame grezzo"; // #92: l'indicatore viene sempre dal frame grezzo
$("raddrizzato").textContent = r === "si" ? "raddrizzata dalla calibrazione" : r === "no" ? "non raddrizzata" : " ";
} }
// 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)
@@ -381,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);
@@ -398,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");
@@ -447,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)
@@ -676,6 +818,10 @@ function scegliTab(nome, salva) {
$("tabCal").tabIndex = cal ? 0 : -1; $("tabCal").tabIndex = cal ? 0 : -1;
$("pannelloAcq").hidden = cal; $("pannelloAcq").hidden = cal;
$("pannelloCal").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 if (salva) { memoria("tmf.acq.tab", nome); if (cal) caricaCalibrazioni(); } // l'elenco si rilegge entrando
} }
$("tabAcq").onclick = function () { scegliTab("acq", true); }; $("tabAcq").onclick = function () { scegliTab("acq", true); };
@@ -770,7 +916,7 @@ async function aggiornaStato() {
stato.appendChild(titolo); stato.appendChild(titolo);
stato.appendChild(t); stato.appendChild(t);
stato.appendChild(quando); stato.appendChild(quando);
$("statoSorgente").textContent = "calibrata: " + fmt(d.px_per_mm, 2) + " px/mm"; $("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 if (d.square_mm) $("quadrato").value = fmtCasella(d.square_mm); // #87: parte dal lato gia' usato
} }
} catch (e) { } catch (e) {
@@ -877,8 +1023,8 @@ async function caricaNome(nome) {
if (sorgente !== $("sorgente").value) return; if (sorgente !== $("sorgente").value) return;
if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; } if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; }
dire("msgElenco", "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok"); dire("msgElenco", "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok");
$("raddrizzato").textContent = $("grezzo").checked ? "non raddrizzata" : "raddrizzata dalla calibrazione"; // il prossimo frame conferma
aggiornaStato(); aggiornaStato();
aggiornaCalibrata(); // #92: il riquadro calibrata usa subito la nuova calibrazione
} catch (e) { } catch (e) {
dire("msgElenco", "Servizio non raggiungibile.", "errore"); dire("msgElenco", "Servizio non raggiungibile.", "errore");
} }
@@ -891,11 +1037,11 @@ async function scollega() {
try { try {
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" }); 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"); dire("msgElenco", r.ok ? "Calibrazione scollegata dalla sorgente (resta fra le salvate)." : await dettaglio(r), r.ok ? "ok" : "errore");
if (r.ok) $("raddrizzato").textContent = "non raddrizzata";
} catch (e) { } catch (e) {
dire("msgElenco", "Servizio non raggiungibile.", "errore"); dire("msgElenco", "Servizio non raggiungibile.", "errore");
} }
aggiornaStato(); 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) // «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser)
@@ -927,25 +1073,18 @@ async function eliminaNome(nome, btn) {
// #91: il calcolo non salva mai; il servizio tiene il risultato e ne da' il calcolo_id per «Salva» // #91: il calcolo non salva mai; il servizio tiene il risultato e ne da' il calcolo_id per «Salva»
async function calcola() { async function calcola() {
var daFile = $("origFile").checked; var sorgente = $("sorgente").value; // #92: sempre la sorgente della tab Acquisizione, frame grezzo
var sorgente = $("sorgente").value; if (!sorgente) { messaggio("Scegliere una sorgente nella tab Acquisizione.", "errore"); return; }
if (!daFile && !sorgente) { messaggio("Scegliere una sorgente nella tab Acquisizione.", "errore"); return; }
var lato = leggiNumero($("quadrato").value); var lato = leggiNumero($("quadrato").value);
if (!(lato > 0)) { messaggio("Il lato dello scacco deve essere un numero maggiore di zero.", "errore"); return; } if (!(lato > 0)) { messaggio("Il lato dello scacco deve essere un numero maggiore di zero.", "errore"); return; }
var f = new FormData(); var f = new FormData();
f.append("square_mm", String(lato)); // al servizio sempre con il punto f.append("square_mm", String(lato)); // al servizio sempre con il punto
if (daFile) { f.append("source", sorgente);
var file = $("file").files[0]; fermaAnteprima(); // #92: in questa tab il ciclo e' gia' sospeso; qui si aspetta anche l'eventuale richiesta ancora in volo
if (!file) { messaggio("Scegliere un file immagine.", "errore"); return; }
f.append("image", file);
} else {
f.append("source", sorgente);
}
var eraAttiva = !daFile && A.attiva; // #87: da sorgente l'anteprima si sospende e poi riprende
if (eraAttiva) fermaAnteprima();
messaggio("Calcolo in corso...", ""); messaggio("Calcolo in corso...", "");
["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; }); ["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; });
try { try {
while (A.inVolo) await pausa(50);
var r = await fetch("/camera/calibrate", { method: "POST", body: f }); var r = await fetch("/camera/calibrate", { method: "POST", body: f });
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; } if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; }
var d = await r.json(); var d = await r.json();
@@ -963,7 +1102,6 @@ async function calcola() {
} finally { } finally {
$("acquisisci").disabled = false; $("acquisisci").disabled = false;
$("salva").disabled = false; $("salva").disabled = false;
if (eraAttiva) avviaAnteprima();
} }
} }
@@ -1007,14 +1145,7 @@ async function salvaNome() {
$("acquisisci").onclick = calcola; $("acquisisci").onclick = calcola;
$("salva").onclick = salvaNome; $("salva").onclick = salvaNome;
$("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); }; $("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); };
// #87: il pulsante del file e' in italiano, l'input nativo resta nascosto $("sorgente").onchange = function () { dire("msgSorgente", ""); cambioSorgente(true); }; // #92
$("scegliFile").onclick = function () { $("file").click(); };
$("file").onchange = function () {
$("nomeFile").textContent = $("file").files[0] ? $("file").files[0].name : "nessuna immagine scelta";
if ($("file").files[0]) $("origFile").checked = true; // #91: scelto un file, l'origine diventa il file
};
$("sorgente").onchange = function () { cambioSorgente(true); };
scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata
caricaSorgenti(); caricaSorgenti();
</script> </script>
+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",
"salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga, nell'elenco "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
+51 -14
View File
@@ -1,4 +1,4 @@
"""Test della pagina a due tab (#91): contenuto statico di PAGINA, senza servizio.""" """Test della pagina a due tab (#91, #92): contenuto statico di PAGINA, senza servizio."""
import importlib.util import importlib.util
import re import re
@@ -20,37 +20,58 @@ def test_due_tab_con_ruolo_e_selezione():
assert '"tmf.acq.tab"' in PAGINA # tab scelta ricordata (via memoria(), protetta) assert '"tmf.acq.tab"' in PAGINA # tab scelta ricordata (via memoria(), protetta)
def test_anteprima_fuori_dai_pannelli(): def test_sorgente_in_cima_poi_riquadri():
# l'anteprima sta nella colonna sinistra, prima dei due pannelli: visibile in entrambe le tab # #92: Sorgente, anteprima e parametri stanno nel pannello Acquisizione; Sorgente prima dei riquadri
i_ant = PAGINA.index('id="sezioneAnteprima"')
i_acq = PAGINA.index('id="pannelloAcq"') i_acq = PAGINA.index('id="pannelloAcq"')
i_cal = PAGINA.index('id="pannelloCal"') i_cal = PAGINA.index('id="pannelloCal"')
assert i_ant < i_acq < i_cal i_sorg = PAGINA.index('id="sezioneSorgente"')
# Sorgente e Parametri nel pannello Acquisizione, la calibrazione in quello Calibrazione i_ant = PAGINA.index('id="sezioneAnteprima"')
assert i_acq < PAGINA.index('id="sezioneSorgente"') < PAGINA.index('id="sezioneParametri"') < i_cal 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 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(): def test_id_del_pannello_calibrazione():
ids = set(re.findall(r'id="([^"]+)"', PAGINA)) ids = set(re.findall(r'id="([^"]+)"', PAGINA))
attesi = {"calSorgente", "origSorgente", "origFile", "scegliFile", "file", "quadrato", "acquisisci", attesi = {"calSorgente", "quadrato", "acquisisci", "nomeCal", "salva", "msgSalva", "calibrazioni",
"nomeCal", "salva", "msgSalva", "calibrazioni", "msgElenco", "stato", "risultato", "overlay"} "msgElenco", "stato", "risultato", "overlay",
"caricaImmagine", "fileImmagine", "rimuoviImmagine", "riquadroCal", "immagineCal", "mirinoCal"}
assert attesi <= ids, attesi - ids assert attesi <= ids, attesi - ids
usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA)) usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
assert usati <= ids, usati - ids assert usati <= ids, usati - ids
assert len(re.findall(r'id="([^"]+)"', PAGINA)) == len(ids) # nessun id doppio 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(): def test_testi_del_pannello():
for testo in ("Sorgente selezionata", "Immagine da file", "Scegli immagine...", "Calcola", for testo in ("Camera o immagine", "Carica immagine da file...", "Rimuovi immagine", "Calcola",
"Calibrazioni salvate", "si cambia nella tab Acquisizione", "risoluzione diversa", "Calibrazioni salvate", "risoluzione diversa", "Calibrata",
"Carica", "Elimina", "Scollega", "Sovrascrivi «", "Clic ancora", "Nome"): "Carica", "Elimina", "Scollega", "Sovrascrivi «", "Clic ancora", "Nome",
"calibrata: ", "non calibrata"):
assert testo in PAGINA, testo assert testo in PAGINA, testo
def test_rotte_del_contratto(): def test_rotte_del_contratto():
for rotta in ("/camera/calibrazioni", "/camera/calibrazione-attiva", "/camera/calibrate", for rotta in ("/camera/calibrazioni", "/camera/calibrazione-attiva", "/camera/calibrate",
"/camera/calibration?source="): "/camera/calibration?source=", "/camera/immagini", "/camera/immagini?nome=",
"/camera/anteprima/calibrata"): # #92
assert '"' + rotta in PAGINA, rotta assert '"' + rotta in PAGINA, rotta
for parola in ("calcolo_id", "sovrascrivi", "method: \"PUT\"", "method: \"DELETE\"", "X-Larghezza", "X-Altezza"): for parola in ("calcolo_id", "sovrascrivi", "method: \"PUT\"", "method: \"DELETE\"", "X-Larghezza", "X-Altezza"):
assert parola in PAGINA, parola assert parola in PAGINA, parola
@@ -65,7 +86,7 @@ def test_nessuna_finestra_del_browser_e_offline():
def test_acquisizione_di_87_90_conservata(): def test_acquisizione_di_87_90_conservata():
# anteprima, indicatori, mirino, grezzo, parametri camera e ciclo a 100 ms restano # anteprima, indicatori, mirino, grezzo, parametri camera e ciclo a 100 ms restano
for parola in ("Parametri camera", "Immagine grezza (senza correzione)", "Mirino centrale", for parola in ("Parametri camera", "Mirino centrale",
"Math.max(100 - (Date.now() - t0), 0)", "/camera/anteprima", "/camera/parametri"): "Math.max(100 - (Date.now() - t0), 0)", "/camera/anteprima", "/camera/parametri"):
assert parola in PAGINA, parola assert parola in PAGINA, parola
@@ -75,3 +96,19 @@ def test_pagina_stretta():
# #91: elenco a schede, niente tabella larga ne' scorrimento orizzontale # #91: elenco a schede, niente tabella larga ne' scorrimento orizzontale
assert 'id="calibrazioni" class="elenco"' in PAGINA and "overflow-x" not in PAGINA assert 'id="calibrazioni" class="elenco"' in PAGINA and "overflow-x" not in PAGINA
assert "Lettura dei parametri..." 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