diff --git a/scripts/calibrazione_pagina.py b/scripts/calibrazione_pagina.py index f65b952..3a67e06 100644 --- a/scripts/calibrazione_pagina.py +++ b/scripts/calibrazione_pagina.py @@ -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 (/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. """ @@ -61,10 +66,19 @@ PAGINA = r""" display: flex; align-items: center; justify-content: center; margin-top: 10px; overflow: hidden; } .cornice { position: relative; display: inline-block; line-height: 0; max-width: 100%; } .cornice img { max-width: 100%; max-height: calc(100vh - 330px); min-height: 200px; border: 0; margin: 0; } - #mirino { position: absolute; inset: 0; pointer-events: none; + .mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */ background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat, linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; } - #vuoto { padding: 30px 16px; color: var(--secondario); text-align: center; } + #vuoto, #vuotoCal { padding: 30px 16px; color: var(--secondario); text-align: center; } + /* #92: due riquadri affiancati (grezza | calibrata); in colonna sotto i 900 px */ + .riquadri { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; } + .riquadri.doppio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } + .riquadro .titolo { font-size: .88rem; font-weight: 600; } + .riquadro .titolo span { font-weight: 400; font-size: .78rem; color: var(--secondario); margin-left: 6px; } + .riquadro .palco { margin-top: 4px; } + .sorgente-riga { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; } + .sorgente-riga .campo-sorgente { flex: 1 1 260px; min-width: 0; } + .sorgente-riga select { width: 100%; } .indicatori { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 10px; } .indicatore { background: var(--sfondo); border: 1px solid var(--bordo); border-radius: 6px; padding: 8px 10px; } .indicatore .nome { font-size: .78rem; color: var(--secondario); } @@ -94,10 +108,9 @@ PAGINA = r""" .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; } - .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) { .griglia { grid-template-columns: minmax(0, 1fr); } + .riquadri.doppio { grid-template-columns: minmax(0, 1fr); } /* #92: in colonna */ .cornice img { max-height: 60vh; } } @media (max-width: 500px) { /* #91: utilizzabile anche stretta (400 px) */ @@ -112,125 +125,134 @@ PAGINA = r"""

Acquisizione camera

- +
-
-
- -
-

Anteprima

-
- - - - - +
+ +
+

Sorgente

+
+
+ +
-
-
Anteprima ferma. Premere «Avvia anteprima» o «Scatta».
- -
-
-
-
Nitidezza
-
-
-
-
massimo visto: - -
-
-
-
Saturati
-
-
-
pixel a 250 o più; da tenere sotto l'1 %
-
-
-
Livello medio
-
-
-
scala 0-255
-
-
-
Frame pieno
-
-
-
 
-
-
-
-
-
+ + + +
+
-
+
+
-
- -
-

Sorgente

- - -
-
-
-
+ +
+

Anteprima

+
+ + + + + +
+
+
+
Immaginegrezza, senza correzione
+
+
Anteprima ferma. Premere «Avvia anteprima» o «Scatta».
+ +
+
+ +
+
+
+
Nitidezza
+
-
+
+
massimo visto: - +
+
+
+
Saturati
+
-
+
pixel a 250 o più; da tenere sotto l'1 %
+
+
+
Livello medio
+
-
+
scala 0-255
+
+
+
Frame pieno
+
-
+
 
+
+
+
+
- -
-

Parametri camera

-
-
- -
-
-
+ +
+

Parametri camera

+
-
+ +
+
+
- - +
+

Calibrazioni salvate

+
-
+
+
+
@@ -289,8 +311,11 @@ async function dettaglio(risposta) { // ---- Sorgente -async function caricaSorgenti() { +// #92: scegli = nome da selezionare (immagine appena caricata); senza, l'ultima scelta ricordata +async function caricaSorgenti(scegli) { var sel = $("sorgente"); + var prima = sel.value; + sel.textContent = ""; try { var r = await fetch("/camera/sources"); var j = await r.json(); @@ -298,23 +323,84 @@ async function caricaSorgenti() { var o = document.createElement("option"); o.value = s.name; o.textContent = s.label || s.name; + o.dataset.kind = s.kind || ""; // #92: «image» = immagine caricata da file sel.appendChild(o); }); } catch (e) { dire("msgSorgente", "Elenco delle sorgenti non disponibile.", "errore"); } if (!sel.options.length) dire("msgSorgente", "Nessuna sorgente camera trovata.", "errore"); - var ricordata = memoria("tmf.acq.sorgente"); // #87: ultima scelta ricordata - if (ricordata) { - for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === ricordata) sel.value = ricordata; + var voluta = scegli || (prima === "" ? memoria("tmf.acq.sorgente") : prima); // #87: ultima scelta ricordata + if (voluta) { + 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 function cambioSorgente(salva) { if (salva) memoria("tmf.acq.sorgente", $("sorgente").value); var era = A.attiva; fermaAnteprima(); azzeraIndicatori(); + svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente + annullaRimuovi(); + $("rimuoviImmagine").hidden = !sorgenteImmagine(); dire("msgAnteprima", ""); dire("msgElenco", ""); // #91 aggiornaStato(); @@ -324,7 +410,8 @@ function cambioSorgente(salva) { // ---- 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() { A.nitMax = 0; @@ -350,6 +437,52 @@ function mostraImmagine(blob) { $("vuoto").hidden = true; } +// #92: riquadro «Calibrata»: l'immagine raddrizzata oppure il motivo per cui non c'e' (404/409 del servizio) +function mostraCalibrata(blob, nome) { + var vecchio = A.urlCal; + A.urlCal = URL.createObjectURL(blob); + $("immagineCal").src = A.urlCal; + if (vecchio) URL.revokeObjectURL(vecchio); + $("corniceCal").hidden = false; + $("vuotoCal").hidden = true; + $("nomeCalibrata").textContent = nome ? "«" + nome + "»" : ""; +} +function messaggioCalibrata(testo) { + $("corniceCal").hidden = true; + $("vuotoCal").hidden = false; + $("vuotoCal").textContent = testo; + $("nomeCalibrata").textContent = ""; +} +// #92: cambio sorgente: via i due frame, nessuna immagine di un'altra sorgente +function svuotaRiquadri() { + A.seqCal++; // una risposta calibrata ancora in volo non vale piu' + $("cornice").hidden = true; + $("vuoto").hidden = false; + messaggioCalibrata("Premere «Avvia anteprima» o «Scatta»."); +} +// #92: raddrizza l'ULTIMO frame grezzo tenuto dal servizio (nessuna lettura nuova); chiamata dopo ogni anteprima, dopo Carica/Scollega e alla casella +async function aggiornaCalibrata(lato) { + var sorgente = $("sorgente").value; + if (!$("mostraCal").checked || !sorgente) return; + var n = ++A.seqCal; + try { + var r = await fetch("/camera/anteprima/calibrata", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ source: sorgente, lato_max: lato || A.lato }) + }); + if (n !== A.seqCal) return; // risposta superata da una piu' recente o da un cambio sorgente + if (!r.ok) { messaggioCalibrata(await dettaglio(r)); return; } // l'anteprima grezza continua + var nome = ""; + try { nome = decodeURIComponent(r.headers.get("X-Calibrazione") || ""); } catch (e) { /* intestazione non decodificabile: nessun nome */ } + var blob = await r.blob(); + if (n !== A.seqCal) return; + mostraCalibrata(blob, nome); + } catch (e) { + if (n === A.seqCal) messaggioCalibrata("Servizio non raggiungibile."); + } +} + function aggiornaIndicatori(h) { var nit = parseFloat(h.get("X-Nitidezza")); var sat = parseFloat(h.get("X-Saturati-Pct")); @@ -372,8 +505,7 @@ function aggiornaIndicatori(h) { 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"); - $("raddrizzato").textContent = r === "si" ? "raddrizzata dalla calibrazione" : r === "no" ? "non raddrizzata" : " "; + $("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) @@ -381,12 +513,13 @@ async function unaAnteprima(lato) { var sorgente = $("sorgente").value; if (!sorgente) { dire("msgAnteprima", "Scegliere una sorgente.", "errore"); return "fatale"; } var g = A.gen; + A.lato = lato; A.inVolo = true; try { var r = await fetch("/camera/anteprima", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ source: sorgente, grezzo: $("grezzo").checked, lato_max: lato, timeout_ms: 5000 }) + body: JSON.stringify({ source: sorgente, grezzo: true, lato_max: lato, timeout_ms: 5000 }) // #92: il riquadro «Immagine» e' sempre grezzo }); if (!r.ok) { var d = await dettaglio(r); @@ -398,6 +531,7 @@ async function unaAnteprima(lato) { mostraImmagine(blob); aggiornaIndicatori(r.headers); dire("msgAnteprima", ""); + await aggiornaCalibrata(lato); // #92: stesso frame, raddrizzato; resta in volo finche' non risponde return "ok"; } catch (e) { if (g === A.gen) dire("msgAnteprima", "Servizio non raggiungibile.", "errore"); @@ -447,7 +581,15 @@ $("avvia").onclick = avviaAnteprima; $("ferma").onclick = fermaAnteprima; $("scatta").onclick = scatta; $("azzeraMax").onclick = function () { A.nitMax = 0; $("nitidezzaMax").textContent = "-"; $("barraNitidezza").style.width = "0"; }; -$("mirinoCasella").onchange = function () { $("mirino").hidden = !$("mirinoCasella").checked; }; +// #92: il mirino vale per entrambi i riquadri; la casella «Mostra immagine calibrata» apre il riquadro e raddrizza il frame gia' tenuto +$("mirinoCasella").onchange = function () { $("mirino").hidden = $("mirinoCal").hidden = !$("mirinoCasella").checked; }; +function mostraRiquadroCal() { + var si = $("mostraCal").checked; + $("riquadroCal").hidden = !si; + $("riquadri").className = "riquadri" + (si ? " doppio" : ""); + if (si) aggiornaCalibrata(); +} +$("mostraCal").onchange = mostraRiquadroCal; // ---- Parametri camera (#87) @@ -676,6 +818,10 @@ function scegliTab(nome, salva) { $("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); }; @@ -770,7 +916,7 @@ async function aggiornaStato() { stato.appendChild(titolo); stato.appendChild(t); 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 } } catch (e) { @@ -877,8 +1023,8 @@ async function caricaNome(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(); + aggiornaCalibrata(); // #92: il riquadro calibrata usa subito la nuova calibrazione } catch (e) { dire("msgElenco", "Servizio non raggiungibile.", "errore"); } @@ -891,11 +1037,11 @@ async function scollega() { 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(); + 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) @@ -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» async function calcola() { - var daFile = $("origFile").checked; - var sorgente = $("sorgente").value; - if (!daFile && !sorgente) { messaggio("Scegliere una sorgente nella tab Acquisizione.", "errore"); return; } + 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 - if (daFile) { - var file = $("file").files[0]; - 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(); + 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(); @@ -963,7 +1102,6 @@ async function calcola() { } finally { $("acquisisci").disabled = false; $("salva").disabled = false; - if (eraAttiva) avviaAnteprima(); } } @@ -1007,14 +1145,7 @@ async function salvaNome() { $("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 -$("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); }; +$("sorgente").onchange = function () { dire("msgSorgente", ""); cambioSorgente(true); }; // #92 scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata caricaSorgenti(); diff --git a/scripts/tests/test_pagina_acquisizione.py b/scripts/tests/test_pagina_acquisizione.py index 247a1f7..e7b9d78 100644 --- a/scripts/tests/test_pagina_acquisizione.py +++ b/scripts/tests/test_pagina_acquisizione.py @@ -29,10 +29,10 @@ def test_offline_e_senza_finestre_del_browser(): def test_id_principali(): 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", - "parametri", "salvaParametri", "quadrato", "acquisisci", "scegliFile", "file", - "salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga, nell'elenco + "parametri", "salvaParametri", "quadrato", "acquisisci", "caricaImmagine", "fileImmagine", + "mostraCal", "salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga; #92: file e calibrata nell'Acquisizione assert attesi <= ids, attesi - ids # ogni id usato dal JS con $("...") esiste nel markup usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA)) @@ -41,8 +41,8 @@ def test_id_principali(): def test_sezioni_e_testi_italiani(): for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione", - "Salva come predefiniti", "Scegli immagine...", "Questa sorgente non ha parametri regolabili", - "Immagine grezza (senza correzione)"): + "Salva come predefiniti", "Carica immagine da file...", "Questa sorgente non ha parametri regolabili", + "Mostra immagine calibrata"): # #92 assert testo in PAGINA, testo assert "it-IT" in PAGINA # numeri con la virgola assert "Choose File" not in PAGINA diff --git a/scripts/tests/test_pagina_tab.py b/scripts/tests/test_pagina_tab.py index e3aa9b7..14d062a 100644 --- a/scripts/tests/test_pagina_tab.py +++ b/scripts/tests/test_pagina_tab.py @@ -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 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) -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"') +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"') - 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 + 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'