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>
This commit is contained in:
+345
-90
@@ -8,6 +8,10 @@ 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.
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -75,16 +79,45 @@ 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 */
|
||||||
|
.scorri { overflow-x: auto; margin-top: 8px; }
|
||||||
|
.elenco { width: 100%; }
|
||||||
|
.elenco th { text-align: left; font-size: .78rem; font-weight: 600; color: var(--secondario); padding: 2px 12px 4px 0; white-space: nowrap; }
|
||||||
|
.elenco td { padding: 5px 12px 5px 0; border-top: 1px solid var(--bordo); vertical-align: top; }
|
||||||
|
.elenco td:first-child { color: var(--testo); }
|
||||||
|
.elenco .azioni { white-space: nowrap; }
|
||||||
|
.elenco button { padding: 3px 9px; font-size: .82rem; margin-right: 4px; }
|
||||||
|
.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; }
|
||||||
|
.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); }
|
||||||
.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>
|
||||||
|
|
||||||
|
<!-- #91: due tab; la colonna dell'anteprima resta visibile in entrambe -->
|
||||||
|
<div class="tabs" role="tablist" aria-label="Sezioni della pagina">
|
||||||
|
<button id="tabAcq" type="button" role="tab" aria-selected="true" aria-controls="pannelloAcq">Acquisizione</button>
|
||||||
|
<button id="tabCal" type="button" role="tab" aria-selected="false" aria-controls="pannelloCal" tabindex="-1">Calibrazione</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="griglia">
|
<div class="griglia">
|
||||||
<div class="colonna">
|
<div class="colonna">
|
||||||
<!-- #87: sezione Anteprima -->
|
<!-- #87: sezione Anteprima -->
|
||||||
@@ -132,7 +165,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="colonna">
|
<div class="colonna" id="pannelloAcq" role="tabpanel" aria-labelledby="tabAcq">
|
||||||
<!-- #87: sezione Sorgente -->
|
<!-- #87: sezione Sorgente -->
|
||||||
<section class="scheda" id="sezioneSorgente">
|
<section class="scheda" id="sezioneSorgente">
|
||||||
<h2>Sorgente</h2>
|
<h2>Sorgente</h2>
|
||||||
@@ -151,43 +184,51 @@ PAGINA = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
<div id="msgParametri" class="messaggio"></div>
|
<div id="msgParametri" class="messaggio"></div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- #91: tab Calibrazione (colonna destra al posto di Sorgente e Parametri) -->
|
||||||
|
<div class="colonna" 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="riga">
|
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> (si cambia nella tab Acquisizione)</div>
|
||||||
|
<div class="origine">
|
||||||
|
<label for="origSorgente"><input id="origSorgente" name="origine" type="radio" value="sorgente" checked> Sorgente selezionata</label>
|
||||||
|
<label for="origFile"><input id="origFile" name="origine" type="radio" value="file"> Immagine da file</label>
|
||||||
|
</div>
|
||||||
|
<div class="riga" style="margin-top:4px">
|
||||||
|
<button id="scegliFile" class="secondario">Scegli immagine...</button>
|
||||||
|
<input id="file" type="file" accept="image/*" hidden>
|
||||||
|
<span id="nomeFile" class="nota" style="margin:0 0 6px">nessuna immagine scelta</span>
|
||||||
|
</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>
|
<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>
|
</div>
|
||||||
<p class="nota">
|
<p class="nota">
|
||||||
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
|
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.
|
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
|
||||||
Durante il calcolo l'anteprima si sospende.</p>
|
Calcolando dalla sorgente l'anteprima si sospende e poi riprende.</p>
|
||||||
<div class="riga" style="margin-top:10px">
|
|
||||||
<button id="acquisisci">Acquisisci e calibra</button>
|
|
||||||
<button id="scegliFile" class="secondario">Scegli immagine...</button>
|
|
||||||
<input id="file" type="file" accept="image/*" hidden>
|
|
||||||
<span id="nomeFile" class="nota" style="margin:0 0 6px">nessuna immagine scelta</span>
|
|
||||||
<button id="carica" class="secondario">Carica immagine e calibra</button>
|
|
||||||
</div>
|
|
||||||
<div id="messaggio" class="messaggio"></div>
|
<div id="messaggio" class="messaggio"></div>
|
||||||
|
|
||||||
<div id="risultato" hidden style="margin-top:10px">
|
<div id="risultato" hidden style="margin-top:10px">
|
||||||
<strong>Risultato del calcolo</strong>
|
<strong>Risultato del calcolo</strong>
|
||||||
|
<div id="origineCalcolo" class="nota" style="margin:2px 0 6px"></div>
|
||||||
<table id="tabella"></table>
|
<table id="tabella"></table>
|
||||||
<div id="avviso" class="avviso"></div>
|
<div id="avviso" class="avviso"></div>
|
||||||
<div class="riga" style="margin-top:10px">
|
<div class="riga" style="margin-top:10px">
|
||||||
|
<div><label for="nomeCal">Nome</label><input id="nomeCal" type="text" maxlength="60" size="22" autocomplete="off"></div>
|
||||||
<button id="salva">Salva</button>
|
<button id="salva">Salva</button>
|
||||||
<span class="nota" style="margin:0 0 6px">Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<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">
|
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div style="margin-top:14px">
|
<section class="scheda" id="sezioneSalvate">
|
||||||
<strong>Calibrazione salvata</strong>
|
<h2>Calibrazioni salvate</h2>
|
||||||
<div id="stato" class="stato">-</div>
|
<div id="stato" class="stato">-</div>
|
||||||
<div class="riga" style="margin-top:10px">
|
<div class="scorri"><table id="calibrazioni" class="elenco"></table></div>
|
||||||
<button id="elimina" class="pericolo" disabled>Elimina calibrazione</button>
|
<div id="msgElenco" class="messaggio"></div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,6 +316,7 @@ function cambioSorgente(salva) {
|
|||||||
fermaAnteprima();
|
fermaAnteprima();
|
||||||
azzeraIndicatori();
|
azzeraIndicatori();
|
||||||
dire("msgAnteprima", "");
|
dire("msgAnteprima", "");
|
||||||
|
dire("msgElenco", ""); // #91
|
||||||
aggiornaStato();
|
aggiornaStato();
|
||||||
caricaParametri();
|
caricaParametri();
|
||||||
if (era) avviaAnteprima();
|
if (era) avviaAnteprima();
|
||||||
@@ -282,7 +324,7 @@ function cambioSorgente(salva) {
|
|||||||
|
|
||||||
// ---- Anteprima (#87)
|
// ---- Anteprima (#87)
|
||||||
|
|
||||||
var A = { attiva: false, gen: 0, inVolo: false, url: null, nitMax: 0 };
|
var A = { attiva: false, gen: 0, inVolo: false, url: null, nitMax: 0, ris: null }; // #91: ris = {w, h} dell'ultimo frame
|
||||||
|
|
||||||
function azzeraIndicatori() {
|
function azzeraIndicatori() {
|
||||||
A.nitMax = 0;
|
A.nitMax = 0;
|
||||||
@@ -290,6 +332,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() {
|
||||||
@@ -322,7 +366,12 @@ 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) {
|
||||||
|
$("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0);
|
||||||
|
// #91: risoluzione nota del frame, per segnare le calibrazioni che non combaciano
|
||||||
|
var nw = parseInt(w, 10), na = parseInt(a, 10);
|
||||||
|
if (!A.ris || A.ris.w !== nw || A.ris.h !== na) { A.ris = { w: nw, h: na }; segnaRisoluzioni(); }
|
||||||
|
}
|
||||||
var r = h.get("X-Raddrizzato");
|
var r = h.get("X-Raddrizzato");
|
||||||
$("raddrizzato").textContent = r === "si" ? "raddrizzata dalla calibrazione" : r === "no" ? "non raddrizzata" : " ";
|
$("raddrizzato").textContent = r === "si" ? "raddrizzata dalla calibrazione" : r === "no" ? "non raddrizzata" : " ";
|
||||||
}
|
}
|
||||||
@@ -611,6 +660,29 @@ $("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;
|
||||||
|
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 +716,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 +728,292 @@ 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");
|
||||||
|
annullaConferma();
|
||||||
|
$("calSorgente").textContent = sorgente ? $("sorgente").options[$("sorgente").selectedIndex].textContent : "-"; // #91
|
||||||
|
if (!sorgente) { stato.textContent = "-"; $("statoSorgente").textContent = "-"; caricaCalibrazioni(); 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";
|
||||||
|
} else if (!r.ok) {
|
||||||
|
stato.textContent = await dettaglio(r);
|
||||||
|
} else {
|
||||||
|
var d = await r.json();
|
||||||
|
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: " + 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.";
|
||||||
|
}
|
||||||
|
caricaCalibrazioni();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #91: elenco unico delle calibrazioni salvate; "attiva" e' quella della sorgente selezionata
|
||||||
|
async function caricaCalibrazioni() {
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
var g = ++C.gen;
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/calibrazioni" + (sorgente ? "?source=" + encodeURIComponent(sorgente) : ""));
|
||||||
|
if (g !== C.gen) return;
|
||||||
|
if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; }
|
||||||
|
var j = await r.json();
|
||||||
|
if (g !== C.gen) return;
|
||||||
|
C.elenco = j.calibrazioni || [];
|
||||||
|
C.attiva = j.attiva || null;
|
||||||
|
disegnaElenco();
|
||||||
|
} catch (e) {
|
||||||
|
if (g === C.gen) dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cella(tr, testo) {
|
||||||
|
var td = document.createElement("td");
|
||||||
|
td.textContent = testo;
|
||||||
|
tr.appendChild(td);
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bottone(testo, classe, fn) {
|
||||||
|
var b = document.createElement("button");
|
||||||
|
b.textContent = testo;
|
||||||
|
b.className = classe || "";
|
||||||
|
b.onclick = fn;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disegnaElenco() {
|
||||||
|
var t = $("calibrazioni");
|
||||||
|
annullaConferma();
|
||||||
|
t.textContent = "";
|
||||||
|
C.celle = [];
|
||||||
|
if (!C.elenco.length) {
|
||||||
|
var trVuota = document.createElement("tr");
|
||||||
|
cella(trVuota, "Nessuna calibrazione salvata.");
|
||||||
|
t.appendChild(trVuota);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var testa = document.createElement("tr");
|
||||||
|
["Nome", "Risoluzione", "px/mm", "Metodo", "Data", "Origine", ""].forEach(function (h) {
|
||||||
|
var th = document.createElement("th");
|
||||||
|
th.textContent = h;
|
||||||
|
testa.appendChild(th);
|
||||||
|
});
|
||||||
|
t.appendChild(testa);
|
||||||
|
C.elenco.forEach(function (c) {
|
||||||
|
var tr = document.createElement("tr");
|
||||||
|
var tdNome = cella(tr, c.nome);
|
||||||
|
var attiva = C.attiva !== null && c.nome === C.attiva;
|
||||||
|
if (attiva) {
|
||||||
|
var s1 = document.createElement("span");
|
||||||
|
s1.className = "segno";
|
||||||
|
s1.textContent = "attiva";
|
||||||
|
tdNome.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;
|
||||||
|
tdNome.appendChild(s2);
|
||||||
|
C.celle.push({ w: Number(c.image_width), h: Number(c.image_height), span: s2 });
|
||||||
|
cella(tr, c.image_width && c.image_height ? fmt(c.image_width, 0) + "×" + fmt(c.image_height, 0) : "-");
|
||||||
|
cella(tr, fmt(c.px_per_mm, 3));
|
||||||
|
cella(tr, c.metodo === "densa" ? "densa" : "planare");
|
||||||
|
cella(tr, c.calibrated_at ? dataItaliana(c.calibrated_at) : "-");
|
||||||
|
cella(tr, descriviOrigine(c.origine));
|
||||||
|
var az = document.createElement("td");
|
||||||
|
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); }));
|
||||||
|
tr.appendChild(az);
|
||||||
|
t.appendChild(tr);
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
$("raddrizzato").textContent = $("grezzo").checked ? "non raddrizzata" : "raddrizzata dalla calibrazione"; // il prossimo frame conferma
|
||||||
|
aggiornaStato();
|
||||||
|
} 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");
|
||||||
|
if (r.ok) $("raddrizzato").textContent = "non raddrizzata";
|
||||||
|
} catch (e) {
|
||||||
|
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
||||||
|
}
|
||||||
|
aggiornaStato();
|
||||||
|
}
|
||||||
|
|
||||||
|
// «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;
|
||||||
|
}
|
||||||
|
annullaConferma();
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/calibrazioni?nome=" + encodeURIComponent(nome), { method: "DELETE" });
|
||||||
|
dire("msgElenco", r.ok ? "Calibrazione «" + nome + "» eliminata." : await dettaglio(r), r.ok ? "ok" : "errore");
|
||||||
|
} catch (e) {
|
||||||
|
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
||||||
|
}
|
||||||
|
aggiornaStato();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #91: il calcolo non salva mai; il servizio tiene il risultato e ne da' il calcolo_id per «Salva»
|
||||||
|
async function calcola() {
|
||||||
|
var daFile = $("origFile").checked;
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
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("source", sorgente);
|
|
||||||
f.append("square_mm", String(lato)); // al servizio sempre con il punto
|
f.append("square_mm", String(lato)); // al servizio sempre con il punto
|
||||||
f.append("save", salva ? "true" : "false");
|
if (daFile) {
|
||||||
if (conFile) {
|
|
||||||
var file = $("file").files[0];
|
var file = $("file").files[0];
|
||||||
if (!file) { messaggio("Scegliere un file immagine.", "errore"); return; }
|
if (!file) { messaggio("Scegliere un file immagine.", "errore"); return; }
|
||||||
f.append("image", file);
|
f.append("image", file);
|
||||||
|
} else {
|
||||||
|
f.append("source", sorgente);
|
||||||
}
|
}
|
||||||
var eraAttiva = A.attiva; // #87: l'anteprima si sospende e poi riprende
|
var eraAttiva = !daFile && A.attiva; // #87: da sorgente l'anteprima si sospende e poi riprende
|
||||||
if (eraAttiva) fermaAnteprima();
|
if (eraAttiva) fermaAnteprima();
|
||||||
messaggio("Calcolo in corso...", "");
|
messaggio("Calcolo in corso...", "");
|
||||||
["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = true; });
|
["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; });
|
||||||
try {
|
try {
|
||||||
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();
|
||||||
ultimo = { conFile: conFile, dati: d };
|
ultimo = { dati: d };
|
||||||
tabella($("tabella"), d);
|
tabella($("tabella"), d);
|
||||||
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
||||||
$("avviso").textContent = avvisi(d);
|
$("avviso").textContent = avvisi(d);
|
||||||
|
$("origineCalcolo").textContent = "Origine: " + descriviOrigine(d.origine);
|
||||||
$("risultato").hidden = false;
|
$("risultato").hidden = false;
|
||||||
messaggio(d.saved ? "Calibrazione salvata." : "Calibrazione riuscita: controllare e salvare.", "ok");
|
annullaSovrascrivi();
|
||||||
if (d.saved) aggiornaStato();
|
dire("msgSalva", "");
|
||||||
|
messaggio("Calcolo riuscito: dare un nome e salvare.", "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
messaggio("Servizio non raggiungibile.", "errore");
|
messaggio("Servizio non raggiungibile.", "errore");
|
||||||
} finally {
|
} finally {
|
||||||
["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = false; });
|
$("acquisisci").disabled = false;
|
||||||
|
$("salva").disabled = false;
|
||||||
if (eraAttiva) avviaAnteprima();
|
if (eraAttiva) avviaAnteprima();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("acquisisci").onclick = function () { calcola(false, false); };
|
// «Salva» con nome; se il nome c'e' gia' il pulsante chiede un secondo clic per sovrascrivere
|
||||||
$("carica").onclick = function () { calcola(true, false); };
|
function annullaSovrascrivi() {
|
||||||
// «Salva» rifa il calcolo con save=true sulla stessa origine (altra acquisizione se da camera)
|
clearTimeout(S.timer);
|
||||||
$("salva").onclick = function () { calcola(ultimo ? ultimo.conFile : false, true); };
|
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(); };
|
||||||
// #87: il pulsante del file e' in italiano, l'input nativo resta nascosto
|
// #87: il pulsante del file e' in italiano, l'input nativo resta nascosto
|
||||||
$("scegliFile").onclick = function () { $("file").click(); };
|
$("scegliFile").onclick = function () { $("file").click(); };
|
||||||
$("file").onchange = function () {
|
$("file").onchange = function () {
|
||||||
$("nomeFile").textContent = $("file").files[0] ? $("file").files[0].name : "nessuna immagine scelta";
|
$("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
|
||||||
|
|
||||||
// #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) {
|
|
||||||
$("elimina").textContent = "Confermi? Clic ancora per eliminare";
|
|
||||||
confermaTimer = setTimeout(annullaConferma, 4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
annullaConferma();
|
|
||||||
var sorgente = $("sorgente").value;
|
|
||||||
try {
|
|
||||||
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" });
|
|
||||||
messaggio(r.ok ? "Calibrazione eliminata." : await dettaglio(r), r.ok ? "ok" : "errore");
|
|
||||||
} catch (e) {
|
|
||||||
messaggio("Servizio non raggiungibile.", "errore");
|
|
||||||
}
|
|
||||||
aggiornaStato();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$("sorgente").onchange = function () { cambioSorgente(true); };
|
$("sorgente").onchange = function () { cambioSorgente(true); };
|
||||||
|
scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata
|
||||||
caricaSorgenti();
|
caricaSorgenti();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def test_id_principali():
|
|||||||
attesi = {"sorgente", "avvia", "ferma", "scatta", "grezzo", "mirinoCasella", "immagine",
|
attesi = {"sorgente", "avvia", "ferma", "scatta", "grezzo", "mirinoCasella", "immagine",
|
||||||
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
|
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
|
||||||
"parametri", "salvaParametri", "quadrato", "acquisisci", "scegliFile", "file",
|
"parametri", "salvaParametri", "quadrato", "acquisisci", "scegliFile", "file",
|
||||||
"carica", "salva", "elimina", "stato", "statoSorgente"}
|
"salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga, nell'elenco
|
||||||
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))
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Test della pagina a due tab (#91): 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_anteprima_fuori_dai_pannelli():
|
||||||
|
# l'anteprima sta nella colonna sinistra, prima dei due pannelli: visibile in entrambe le tab
|
||||||
|
i_ant = PAGINA.index('id="sezioneAnteprima"')
|
||||||
|
i_acq = PAGINA.index('id="pannelloAcq"')
|
||||||
|
i_cal = PAGINA.index('id="pannelloCal"')
|
||||||
|
assert i_ant < i_acq < i_cal
|
||||||
|
# Sorgente e Parametri nel pannello Acquisizione, la calibrazione in quello Calibrazione
|
||||||
|
assert i_acq < PAGINA.index('id="sezioneSorgente"') < PAGINA.index('id="sezioneParametri"') < i_cal
|
||||||
|
assert PAGINA.index('id="sezioneCalibrazione"') > i_cal
|
||||||
|
|
||||||
|
|
||||||
|
def test_id_del_pannello_calibrazione():
|
||||||
|
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||||
|
attesi = {"calSorgente", "origSorgente", "origFile", "scegliFile", "file", "quadrato", "acquisisci",
|
||||||
|
"nomeCal", "salva", "msgSalva", "calibrazioni", "msgElenco", "stato", "risultato", "overlay"}
|
||||||
|
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_testi_del_pannello():
|
||||||
|
for testo in ("Sorgente selezionata", "Immagine da file", "Scegli immagine...", "Calcola",
|
||||||
|
"Calibrazioni salvate", "si cambia nella tab Acquisizione", "risoluzione diversa",
|
||||||
|
"Carica", "Elimina", "Scollega", "Sovrascrivi «", "Clic ancora", "Nome"):
|
||||||
|
assert testo in PAGINA, testo
|
||||||
|
|
||||||
|
|
||||||
|
def test_rotte_del_contratto():
|
||||||
|
for rotta in ("/camera/calibrazioni", "/camera/calibrazione-attiva", "/camera/calibrate",
|
||||||
|
"/camera/calibration?source="):
|
||||||
|
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", "Immagine grezza (senza correzione)", "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
|
||||||
|
assert "overflow-x: auto" in PAGINA # l'elenco scorre di lato invece di rompere la pagina
|
||||||
Reference in New Issue
Block a user