Files
zibo-dashboard/public/userarea/cad-area.php
T

2643 lines
90 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php include('include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
ORDER BY id DESC
");
$stmt->execute();
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE iduser = :iduser
ORDER BY id DESC
");
$stmt->execute([
':iduser' => $iduser
]);
}
$jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Calcolo Area CAD - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-add:hover {
background-color: #0b5ed7;
color: #fff;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
.drop-zone {
border: 2px dashed #9fc5f8;
background: #f4f8ff;
border-radius: 16px;
padding: 38px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.drop-zone:hover,
.drop-zone.dragover {
background: #e8f2ff;
border-color: #0d6efd;
transform: translateY(-2px);
}
.drop-zone-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.file-preview {
border: 1px solid #dbeafe;
background: #ffffff;
border-radius: 12px;
padding: 10px 14px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-preview small {
color: #64748b;
}
.status-badge {
font-size: 0.85rem;
padding: 6px 10px;
border-radius: 999px;
}
#cadAreaTable {
table-layout: fixed;
width: 100% !important;
}
#cadAreaTable th,
#cadAreaTable td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
#cadAreaTable th:nth-child(1),
#cadAreaTable td:nth-child(1) {
width: 55px;
}
#cadAreaTable th:nth-child(2),
#cadAreaTable td:nth-child(2) {
width: 260px;
}
#cadAreaTable th:nth-child(3),
#cadAreaTable td:nth-child(3) {
width: 130px;
}
#cadAreaTable th:nth-child(4),
#cadAreaTable td:nth-child(4),
#cadAreaTable th:nth-child(5),
#cadAreaTable td:nth-child(5),
#cadAreaTable th:nth-child(6),
#cadAreaTable td:nth-child(6) {
width: 120px;
}
#cadAreaTable th:nth-child(7),
#cadAreaTable td:nth-child(7) {
width: 160px;
}
#cadAreaTable th:nth-child(8),
#cadAreaTable td:nth-child(8) {
width: 360px;
}
.processing-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
z-index: 9999;
display: none;
align-items: center;
justify-content: center;
}
.processing-box {
background: white;
border-radius: 18px;
padding: 30px;
min-width: 320px;
text-align: center;
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.25);
}
.manual-modal-body {
background: #f8fafc;
overflow: auto;
max-height: 78vh;
text-align: center;
}
.pdf-manual-wrapper {
position: relative;
display: inline-block;
border: 1px solid #cbd5e1;
background: white;
}
#manualPdfCanvas {
display: block;
}
#manualOverlayCanvas {
position: absolute;
left: 0;
top: 0;
cursor: crosshair;
}
.manual-info-box {
background: #eef6ff;
border: 1px solid #bfdbfe;
border-radius: 12px;
padding: 10px 14px;
font-size: 0.95rem;
}
.manual-toolbar {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.manual-toolbar .btn.active {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.2);
font-weight: 700;
}
.manual-result-box {
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 12px;
padding: 10px 14px;
font-size: 0.95rem;
text-align: left;
}
.action-buttons {
display: flex;
gap: 6px;
justify-content: center;
flex-wrap: wrap;
}
.manual-view-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #fff7ed;
border: 1px solid #fed7aa;
color: #9a3412;
border-radius: 999px;
padding: 7px 12px;
font-size: 0.9rem;
font-weight: 600;
}
.manual-edit-help {
background: #f8fafc;
border: 1px dashed #cbd5e1;
color: #475569;
border-radius: 10px;
padding: 8px 12px;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3 mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Calcolo Area 2D da PDF CAD</h5>
<small class="text-muted">
Modalità affidabile: calibrazione quota + tracciamento manuale del profilo.
</small>
</div>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div id="dropZone" class="drop-zone">
<div class="drop-zone-icon">📐</div>
<h5 class="mb-1">Trascina qui uno o più PDF CAD</h5>
<p class="text-muted mb-2">Oppure clicca per selezionare i file</p>
<small class="text-muted">Formati accettati: PDF - massimo 25 MB per file</small>
<input type="file" id="pdfFiles" accept="application/pdf,.pdf" multiple hidden>
</div>
<div id="selectedFiles" class="mt-3"></div>
<div class="text-end mt-3">
<button id="uploadBtn" class="btn btn-add" disabled>
⬆️ Carica PDF
</button>
</div>
</div>
</div>
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Risultati</h5>
<small class="text-muted">
Per PDF scansionati/sporchi usa “Traccia Manuale”.
</small>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="cadAreaTable" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>#</th>
<th>Nome PDF</th>
<th>Stato</th>
<th>Area mm²</th>
<th>Area cm²</th>
<th>Metodo</th>
<th>Confidenza</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($jobs as $job): ?>
<?php
$status = $job['status'] ?? 'uploaded';
if ($status === 'completed') {
$badge = "<span class='badge bg-success status-badge'>Completato</span>";
} elseif ($status === 'processing') {
$badge = "<span class='badge bg-warning text-dark status-badge'>In lavorazione</span>";
} elseif ($status === 'error') {
$badge = "<span class='badge bg-danger status-badge'>Errore</span>";
} else {
$badge = "<span class='badge bg-secondary status-badge'>Caricato</span>";
}
$areaMm2 = $job['area_mm2'] !== null ? number_format((float)$job['area_mm2'], 3, ',', '.') : '-';
$areaCm2 = $job['area_cm2'] !== null ? number_format((float)$job['area_cm2'], 4, ',', '.') : '-';
$method = $job['strategy_used'] ?: '-';
$confidence = $job['confidence'] ?: '-';
$fileUrl = htmlspecialchars($job['file_url'] ?? '', ENT_QUOTES, 'UTF-8');
?>
<tr data-id="<?= (int)$job['id']; ?>">
<td><?= (int)$job['id']; ?></td>
<td title="<?= htmlspecialchars($job['original_filename'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($job['original_filename'], ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><?= $badge; ?></td>
<td class="fw-semibold"><?= $areaMm2; ?></td>
<td class="fw-semibold"><?= $areaCm2; ?></td>
<td><?= htmlspecialchars((string)$method, ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars((string)$confidence, ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<div class="action-buttons">
<?php if (!empty($job['file_url'])): ?>
<a href="<?= $fileUrl; ?>" target="_blank" class="btn btn-sm btn-outline-dark">
📄 Apri
</a>
<button
class="btn btn-sm btn-outline-success manual-trace"
data-id="<?= (int)$job['id']; ?>"
data-url="<?= $fileUrl; ?>">
✏️ Traccia Manuale
</button>
<?php endif; ?>
<button class="btn btn-sm btn-outline-danger delete-one" data-id="<?= (int)$job['id']; ?>">
🗑️
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<div class="processing-overlay" id="processingOverlay">
<div class="processing-box">
<div class="spinner-border text-primary mb-3" role="status"></div>
<h5 class="mb-1">Operazione in corso</h5>
<p class="text-muted mb-0">Attendere...</p>
</div>
</div>
<div class="modal fade" id="manualTraceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-fullscreen modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title mb-0">Tracciamento Manuale Calibrato</h5>
<small class="text-muted">
1) ROI sezione → 2) calibra quota → 3) traccia profilo → 4) calcola e salva area 2D
</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body manual-modal-body">
<div class="manual-info-box mb-3 text-start">
<strong>Procedura:</strong>
seleziona la zona della sezione, calibra usando una quota reale nota, poi clicca i punti del contorno del profilo.
</div>
<div class="manual-toolbar mb-3">
<button type="button" id="toolRoiBtn" class="btn btn-outline-primary active">
🎯 1. ROI
</button>
<button type="button" id="toolCalibrationBtn" class="btn btn-outline-danger">
📏 2. Calibra quota
</button>
<button type="button" id="toolPolygonBtn" class="btn btn-outline-success">
✏️ 3. Disegna profilo
</button>
<button type="button" id="toolHoleBtn" class="btn btn-outline-danger">
4. Area da escludere
</button>
<button type="button" id="closeHoleBtn" class="btn btn-outline-danger">
✅ Chiudi esclusione
</button>
<button type="button" id="toolEditBtn" class="btn btn-outline-secondary">
✋ Modifica punti
</button>
<button type="button" id="undoPointBtn" class="btn btn-outline-secondary">
↩️ Annulla punto
</button>
<button type="button" id="deleteSelectedPointBtn" class="btn btn-outline-danger">
❌ Elimina punto
</button>
<button type="button" id="resetManualBtn" class="btn btn-outline-warning">
🔄 Reset
</button>
<button type="button" id="zoomRoiBtn" class="btn btn-outline-info">
🔍 Zoom su ROI
</button>
<button type="button" id="toolAutoContourRoiBtn" class="btn btn-outline-info">
🎯 ROI autocontorno
</button>
<button type="button" id="autoContourBtn" class="btn btn-outline-primary">
🤖 Proponi contorno
</button>
<button type="button" id="fullPageBtn" class="btn btn-outline-dark" disabled>
↩️ Pagina intera
</button>
<span id="manualViewBadge" class="manual-view-badge">
Vista: pagina intera
</span>
<button type="button" id="calculateManualBtn" class="btn btn-add">
🧮 Calcola area
</button>
</div>
<div class="manual-result-box mb-3" id="manualStatusBox">
Modalità attiva: ROI. Disegna un rettangolo attorno alla sezione.
</div>
<div class="pdf-manual-wrapper" id="manualPdfWrapper">
<canvas id="manualPdfCanvas"></canvas>
<canvas id="manualOverlayCanvas"></canvas>
</div>
</div>
<div class="modal-footer">
<div class="me-auto text-muted" id="manualCoordsPreview">
Nessun dato calcolato.
</div>
<button type="button" class="btn btn-outline-dark" data-bs-dismiss="modal">
Chiudi
</button>
<button type="button" id="saveManualAreaBtn" class="btn btn-add" disabled>
💾 Salva area
</button>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
let selectedFiles = [];
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('pdfFiles');
const selectedFilesBox = document.getElementById('selectedFiles');
const uploadBtn = document.getElementById('uploadBtn');
let manualModal = null;
let currentManualJobId = null;
let currentPdfUrl = null;
let manualPdfCanvas = null;
let manualOverlayCanvas = null;
let manualPdfCtx = null;
let manualOverlayCtx = null;
let manualPdfDoc = null;
let manualPdfPage = null;
let manualBaseScale = 1;
let manualCurrentView = 'full';
let manualRoiPageRect = null;
const MANUAL_FULL_MAX_WIDTH = 1400;
const MANUAL_ROI_ZOOM_FACTOR = 3.5;
let currentTool = 'roi';
let isDrawingRoi = false;
let roiStartX = 0;
let roiStartY = 0;
let roiRect = null;
let isDrawingAutoContourRoi = false;
let autoContourRoiStartX = 0;
let autoContourRoiStartY = 0;
let autoContourRoiRect = null;
let calibrationPoints = [];
let calibrationMm = null;
let calibrationPx = null;
let mmPerPx = null;
let polygonPoints = [];
let currentHolePoints = [];
let holes = [];
let selectedEditPoint = null;
let isDraggingEditPoint = false;
let editDragMoved = false;
const POINT_HIT_RADIUS = 12;
const SEGMENT_HIT_RADIUS = 12;
let lastManualResult = null;
$(document).ready(function() {
$('#cadAreaTable').DataTable({
order: [
[0, 'desc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
});
dropZone.addEventListener('click', function() {
fileInput.click();
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', function() {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', function(e) {
handleFiles(e.target.files);
});
function handleFiles(files) {
const incomingFiles = Array.from(files);
incomingFiles.forEach(file => {
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
if (!isPdf) {
Swal.fire({
icon: 'warning',
title: 'File non valido',
text: `${file.name} non è un PDF.`
});
return;
}
if (file.size > 25 * 1024 * 1024) {
Swal.fire({
icon: 'warning',
title: 'File troppo grande',
text: `${file.name} supera il limite di 25 MB.`
});
return;
}
selectedFiles.push(file);
});
renderSelectedFiles();
}
function renderSelectedFiles() {
selectedFilesBox.innerHTML = '';
selectedFiles.forEach((file, index) => {
const sizeMb = (file.size / 1024 / 1024).toFixed(2);
const item = document.createElement('div');
item.className = 'file-preview';
item.innerHTML = `
<div>
<strong>📄 ${escapeHtml(file.name)}</strong><br>
<small>${sizeMb} MB</small>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeSelectedFile(${index})">
Rimuovi
</button>
`;
selectedFilesBox.appendChild(item);
});
uploadBtn.disabled = selectedFiles.length === 0;
}
function removeSelectedFile(index) {
selectedFiles.splice(index, 1);
renderSelectedFiles();
}
uploadBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
return;
}
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append('pdf_files[]', file);
});
showOverlay();
fetch('cad_area_upload.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
hideOverlay();
if (!data.success) {
Swal.fire({
icon: 'error',
title: 'Errore upload',
text: data.message || 'Upload non riuscito'
});
return;
}
selectedFiles = [];
renderSelectedFiles();
Swal.fire({
icon: 'success',
title: 'PDF caricati',
text: 'Ora usa Traccia Manuale per calcolare larea.',
confirmButtonText: 'OK'
}).then(() => {
location.reload();
});
})
.catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore',
text: error.message || 'Errore durante upload'
});
});
});
$(document).on('click', '.delete-one', function() {
const id = parseInt($(this).data('id'), 10);
Swal.fire({
icon: 'warning',
title: 'Eliminare il PDF?',
text: 'Il file e il relativo risultato saranno rimossi.',
showCancelButton: true,
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla'
}).then(result => {
if (!result.isConfirmed) {
return;
}
fetch('cad_area_delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: data.message || 'Eliminazione non riuscita'
});
}
});
});
});
$(document).on('click', '.manual-trace', function() {
currentManualJobId = parseInt($(this).data('id'), 10);
currentPdfUrl = $(this).data('url');
resetManualState();
manualModal = new bootstrap.Modal(document.getElementById('manualTraceModal'));
manualModal.show();
setTimeout(() => {
loadPdfForManualTracing(currentPdfUrl);
}, 300);
});
function loadPdfForManualTracing(pdfUrl) {
manualPdfCanvas = document.getElementById('manualPdfCanvas');
manualOverlayCanvas = document.getElementById('manualOverlayCanvas');
manualPdfCtx = manualPdfCanvas.getContext('2d');
manualOverlayCtx = manualOverlayCanvas.getContext('2d');
setManualStatus('Caricamento PDF...');
pdfjsLib.getDocument(pdfUrl).promise.then(function(pdfDoc) {
manualPdfDoc = pdfDoc;
return manualPdfDoc.getPage(1);
}).then(function(page) {
manualPdfPage = page;
const viewportOriginal = manualPdfPage.getViewport({
scale: 1
});
manualBaseScale = MANUAL_FULL_MAX_WIDTH / viewportOriginal.width;
manualCurrentView = 'full';
manualRoiPageRect = null;
return renderManualFullPage(false);
}).then(() => {
setupManualCanvasEvents();
redrawManualOverlay();
setTool('roi');
updateManualViewBadge();
}).catch(function(error) {
Swal.fire({
icon: 'error',
title: 'Errore PDF',
text: error.message || 'Impossibile caricare il PDF'
});
});
}
function renderManualFullPage(resetDrawing) {
if (!manualPdfPage) {
return Promise.resolve();
}
if (resetDrawing) {
resetDrawingAfterViewChange();
}
manualCurrentView = 'full';
manualRoiPageRect = null;
const viewport = manualPdfPage.getViewport({
scale: manualBaseScale
});
manualPdfCanvas.width = viewport.width;
manualPdfCanvas.height = viewport.height;
manualOverlayCanvas.width = viewport.width;
manualOverlayCanvas.height = viewport.height;
manualPdfCtx.setTransform(1, 0, 0, 1, 0, 0);
manualPdfCtx.clearRect(0, 0, manualPdfCanvas.width, manualPdfCanvas.height);
return manualPdfPage.render({
canvasContext: manualPdfCtx,
viewport: viewport
}).promise.then(() => {
updateManualViewBadge();
redrawManualOverlay();
});
}
function renderManualRoiViewFromCurrentRoi() {
if (!manualPdfPage || !roiRect || roiRect.w <= 0 || roiRect.h <= 0) {
Swal.fire({
icon: 'warning',
title: 'ROI mancante',
text: 'Prima disegna un rettangolo ROI intorno alla sezione da ingrandire.'
});
return;
}
if (manualCurrentView !== 'full') {
Swal.fire({
icon: 'info',
title: 'Già in zoom ROI',
text: 'Se vuoi scegliere una nuova ROI torna prima alla pagina intera.'
});
return;
}
const pageX = roiRect.x / manualBaseScale;
const pageY = roiRect.y / manualBaseScale;
const pageW = roiRect.w / manualBaseScale;
const pageH = roiRect.h / manualBaseScale;
if (pageW <= 0 || pageH <= 0) {
Swal.fire({
icon: 'warning',
title: 'ROI non valida',
text: 'Disegna una ROI più ampia.'
});
return;
}
manualRoiPageRect = {
x: pageX,
y: pageY,
w: pageW,
h: pageH
};
showOverlay();
renderManualRoiView(true).then(() => {
hideOverlay();
setTool('calibration');
setManualStatus('Zoom su ROI attivo. Ora calibra una quota nota sulla sezione ingrandita.');
updateManualPreview();
}).catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore zoom ROI',
text: error.message || 'Impossibile ingrandire la ROI.'
});
});
}
function renderManualRoiView(resetDrawing) {
if (!manualPdfPage || !manualRoiPageRect) {
return Promise.resolve();
}
if (resetDrawing) {
resetDrawingAfterViewChange();
}
manualCurrentView = 'roi';
const scale = manualBaseScale * MANUAL_ROI_ZOOM_FACTOR;
const viewport = manualPdfPage.getViewport({
scale: scale
});
const canvasWidth = Math.max(50, Math.ceil(manualRoiPageRect.w * scale));
const canvasHeight = Math.max(50, Math.ceil(manualRoiPageRect.h * scale));
manualPdfCanvas.width = canvasWidth;
manualPdfCanvas.height = canvasHeight;
manualOverlayCanvas.width = canvasWidth;
manualOverlayCanvas.height = canvasHeight;
manualPdfCtx.setTransform(1, 0, 0, 1, 0, 0);
manualPdfCtx.clearRect(0, 0, manualPdfCanvas.width, manualPdfCanvas.height);
manualPdfCtx.save();
manualPdfCtx.translate(
-manualRoiPageRect.x * scale,
-manualRoiPageRect.y * scale
);
return manualPdfPage.render({
canvasContext: manualPdfCtx,
viewport: viewport
}).promise.then(() => {
manualPdfCtx.restore();
updateManualViewBadge();
redrawManualOverlay();
}).catch(error => {
manualPdfCtx.restore();
throw error;
});
}
function resetDrawingAfterViewChange() {
isDrawingRoi = false;
roiStartX = 0;
roiStartY = 0;
roiRect = null;
isDrawingAutoContourRoi = false;
autoContourRoiStartX = 0;
autoContourRoiStartY = 0;
autoContourRoiRect = null;
calibrationPoints = [];
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
polygonPoints = [];
currentHolePoints = [];
holes = [];
selectedEditPoint = null;
isDraggingEditPoint = false;
editDragMoved = false;
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
document.getElementById('manualCoordsPreview').innerText = 'Nessun dato calcolato.';
}
function updateManualViewBadge() {
const badge = document.getElementById('manualViewBadge');
const fullPageBtn = document.getElementById('fullPageBtn');
if (!badge || !fullPageBtn) {
return;
}
if (manualCurrentView === 'roi') {
badge.innerText = 'Vista: ROI ingrandita';
fullPageBtn.disabled = false;
} else {
badge.innerText = 'Vista: pagina intera';
fullPageBtn.disabled = true;
}
}
function resetManualState() {
currentTool = 'roi';
isDrawingRoi = false;
roiStartX = 0;
roiStartY = 0;
roiRect = null;
isDrawingAutoContourRoi = false;
autoContourRoiStartX = 0;
autoContourRoiStartY = 0;
autoContourRoiRect = null;
calibrationPoints = [];
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
polygonPoints = [];
currentHolePoints = [];
holes = [];
lastManualResult = null;
manualCurrentView = 'full';
manualRoiPageRect = null;
updateManualViewBadge();
document.getElementById('saveManualAreaBtn').disabled = true;
document.getElementById('manualCoordsPreview').innerText = 'Nessun dato calcolato.';
setTool('roi');
}
function setupManualCanvasEvents() {
manualOverlayCanvas.onmousedown = function(e) {
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
if (currentTool === 'roi') {
isDrawingRoi = true;
roiStartX = pos.x;
roiStartY = pos.y;
roiRect = {
x: roiStartX,
y: roiStartY,
w: 0,
h: 0
};
return;
}
if (currentTool === 'auto_roi') {
isDrawingAutoContourRoi = true;
autoContourRoiStartX = pos.x;
autoContourRoiStartY = pos.y;
autoContourRoiRect = {
x: autoContourRoiStartX,
y: autoContourRoiStartY,
w: 0,
h: 0
};
return;
}
if (currentTool === 'edit') {
selectedEditPoint = findNearestEditablePoint(pos, POINT_HIT_RADIUS);
if (selectedEditPoint) {
isDraggingEditPoint = true;
editDragMoved = false;
updateSelectedPointPosition(pos);
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
} else {
redrawManualOverlay();
setManualStatus('Modifica punti: clicca un punto per selezionarlo, trascinalo per spostarlo, doppio click su un segmento per inserire un nuovo punto.');
}
return;
}
};
manualOverlayCanvas.onmousemove = function(e) {
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
if (currentTool === 'roi' && isDrawingRoi) {
roiRect = {
x: Math.min(roiStartX, pos.x),
y: Math.min(roiStartY, pos.y),
w: Math.abs(pos.x - roiStartX),
h: Math.abs(pos.y - roiStartY)
};
redrawManualOverlay();
return;
}
if (currentTool === 'auto_roi' && isDrawingAutoContourRoi) {
autoContourRoiRect = {
x: Math.min(autoContourRoiStartX, pos.x),
y: Math.min(autoContourRoiStartY, pos.y),
w: Math.abs(pos.x - autoContourRoiStartX),
h: Math.abs(pos.y - autoContourRoiStartY)
};
redrawManualOverlay();
return;
}
if (currentTool === 'edit' && isDraggingEditPoint && selectedEditPoint) {
updateSelectedPointPosition(pos);
editDragMoved = true;
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
return;
}
};
manualOverlayCanvas.onmouseup = function() {
if (currentTool === 'roi' && isDrawingRoi) {
isDrawingRoi = false;
if (roiRect && (roiRect.w < 5 || roiRect.h < 5)) {
roiRect = null;
}
redrawManualOverlay();
updateManualPreview();
if (roiRect) {
setManualStatus('ROI definita. Ora puoi fare “Zoom su ROI” oppure calibrare una quota nota.');
}
return;
}
if (currentTool === 'auto_roi' && isDrawingAutoContourRoi) {
isDrawingAutoContourRoi = false;
if (autoContourRoiRect && (autoContourRoiRect.w < 8 || autoContourRoiRect.h < 8)) {
autoContourRoiRect = null;
}
redrawManualOverlay();
updateManualPreview();
if (autoContourRoiRect) {
setManualStatus('ROI autocontorno definita. Ora clicca “Proponi contorno”: Python analizzerà solo questa zona, senza quote/testi esterni.');
}
return;
}
if (currentTool === 'edit' && isDraggingEditPoint) {
isDraggingEditPoint = false;
if (selectedEditPoint) {
const label = getSelectedPointLabel(selectedEditPoint);
if (editDragMoved) {
setManualStatus(`Punto spostato (${label}). Puoi calcolare di nuovo oppure continuare a modificare.`);
} else {
setManualStatus(`Punto selezionato (${label}). Trascinalo per spostarlo, usa “Elimina punto”, oppure doppio click su un segmento per inserire un punto.`);
}
}
editDragMoved = false;
redrawManualOverlay();
updateManualPreview();
return;
}
};
manualOverlayCanvas.onclick = function(e) {
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
if (currentTool === 'edit') {
selectedEditPoint = findNearestEditablePoint(pos, POINT_HIT_RADIUS);
redrawManualOverlay();
if (selectedEditPoint) {
setManualStatus(`Punto selezionato (${getSelectedPointLabel(selectedEditPoint)}). Trascina per spostarlo o clicca “Elimina punto”.`);
} else {
setManualStatus('Nessun punto selezionato. Clicca vicino a un punto oppure doppio click su un segmento per aggiungerne uno.');
}
return;
}
if (currentTool === 'calibration') {
handleCalibrationClick(pos);
return;
}
if (currentTool === 'polygon') {
handlePolygonClick(pos);
return;
}
if (currentTool === 'hole') {
handleHoleClick(pos);
return;
}
};
manualOverlayCanvas.ondblclick = function(e) {
if (currentTool !== 'edit') {
return;
}
e.preventDefault();
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
const inserted = insertPointOnNearestSegment(pos, SEGMENT_HIT_RADIUS);
if (inserted) {
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
setManualStatus(`Nuovo punto inserito (${getSelectedPointLabel(selectedEditPoint)}). Ora puoi trascinarlo per rifinire il profilo.`);
} else {
Swal.fire({
icon: 'info',
title: 'Nessun segmento vicino',
text: 'Fai doppio click vicino a una linea del contorno esterno o di una esclusione.'
});
}
};
}
function handleCalibrationClick(pos) {
if (calibrationPoints.length >= 2) {
calibrationPoints = [];
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
}
calibrationPoints.push(pos);
redrawManualOverlay();
if (calibrationPoints.length === 2) {
calibrationPx = distance(calibrationPoints[0], calibrationPoints[1]);
let rawValue = window.prompt(
'Inserisci il valore reale della quota in mm.\n\nEsempi: 16 oppure 9.2 oppure 9,2',
''
);
if (rawValue === null) {
calibrationPoints = [];
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
redrawManualOverlay();
updateManualPreview();
return;
}
rawValue = rawValue.trim().replace(',', '.');
const value = parseFloat(rawValue);
if (!rawValue || isNaN(value) || value <= 0) {
calibrationPoints = [];
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
redrawManualOverlay();
updateManualPreview();
Swal.fire({
icon: 'warning',
title: 'Valore non valido',
text: 'Inserisci un valore numerico valido in mm.'
});
return;
}
calibrationMm = value;
mmPerPx = calibrationMm / calibrationPx;
setManualStatus(
`Calibrazione completata: ${calibrationMm} mm = ${calibrationPx.toFixed(2)} px. Ora disegna il profilo.`
);
setTool('polygon');
redrawManualOverlay();
updateManualPreview();
}
}
function handlePolygonClick(pos) {
if (!mmPerPx) {
Swal.fire({
icon: 'warning',
title: 'Calibrazione mancante',
text: 'Prima devi calibrare una quota nota.'
});
return;
}
polygonPoints.push(pos);
selectedEditPoint = {
type: 'outer',
pointIndex: polygonPoints.length - 1
};
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
}
function handleHoleClick(pos) {
if (!mmPerPx) {
Swal.fire({
icon: 'warning',
title: 'Calibrazione mancante',
text: 'Prima devi calibrare una quota nota.'
});
return;
}
if (polygonPoints.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Contorno esterno mancante',
text: 'Prima disegna il contorno esterno del profilo.'
});
return;
}
currentHolePoints.push(pos);
selectedEditPoint = {
type: 'currentHole',
pointIndex: currentHolePoints.length - 1
};
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
}
function closeCurrentHole() {
if (currentHolePoints.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Esclusione incompleta',
text: 'Servono almeno 3 punti per chiudere unarea da escludere.'
});
return;
}
holes.push([...currentHolePoints]);
currentHolePoints = [];
selectedEditPoint = null;
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
setManualStatus(`Area da escludere salvata. Esclusioni presenti: ${holes.length}.`);
}
function redrawManualOverlay() {
if (!manualOverlayCtx || !manualOverlayCanvas) {
return;
}
manualOverlayCtx.clearRect(0, 0, manualOverlayCanvas.width, manualOverlayCanvas.height);
drawRoi();
drawAutoContourRoi();
drawCalibration();
drawPolygon();
drawHoles();
}
function drawRoi() {
if (!roiRect) {
return;
}
manualOverlayCtx.fillStyle = 'rgba(13, 110, 253, 0.12)';
manualOverlayCtx.strokeStyle = 'rgba(13, 110, 253, 0.95)';
manualOverlayCtx.lineWidth = 2;
manualOverlayCtx.fillRect(roiRect.x, roiRect.y, roiRect.w, roiRect.h);
manualOverlayCtx.strokeRect(roiRect.x, roiRect.y, roiRect.w, roiRect.h);
}
function drawAutoContourRoi() {
if (!autoContourRoiRect) {
return;
}
manualOverlayCtx.fillStyle = 'rgba(168, 85, 247, 0.12)';
manualOverlayCtx.strokeStyle = 'rgba(168, 85, 247, 0.98)';
manualOverlayCtx.lineWidth = 2;
manualOverlayCtx.setLineDash([8, 4]);
manualOverlayCtx.fillRect(
autoContourRoiRect.x,
autoContourRoiRect.y,
autoContourRoiRect.w,
autoContourRoiRect.h
);
manualOverlayCtx.strokeRect(
autoContourRoiRect.x,
autoContourRoiRect.y,
autoContourRoiRect.w,
autoContourRoiRect.h
);
manualOverlayCtx.setLineDash([]);
manualOverlayCtx.fillStyle = 'rgba(168, 85, 247, 1)';
manualOverlayCtx.font = '13px Arial';
manualOverlayCtx.fillText(
'ROI autocontorno',
autoContourRoiRect.x + 6,
Math.max(14, autoContourRoiRect.y - 6)
);
}
function drawCalibration() {
if (calibrationPoints.length === 0) {
return;
}
manualOverlayCtx.strokeStyle = 'rgba(220, 38, 38, 0.95)';
manualOverlayCtx.fillStyle = 'rgba(220, 38, 38, 1)';
manualOverlayCtx.lineWidth = 3;
calibrationPoints.forEach(point => {
drawPoint(point, 'rgba(220, 38, 38, 1)', 5);
});
if (calibrationPoints.length === 2) {
manualOverlayCtx.beginPath();
manualOverlayCtx.moveTo(calibrationPoints[0].x, calibrationPoints[0].y);
manualOverlayCtx.lineTo(calibrationPoints[1].x, calibrationPoints[1].y);
manualOverlayCtx.stroke();
}
}
function drawPolygon() {
if (polygonPoints.length === 0) {
return;
}
manualOverlayCtx.strokeStyle = 'rgba(22, 163, 74, 0.95)';
manualOverlayCtx.fillStyle = 'rgba(22, 163, 74, 0.16)';
manualOverlayCtx.lineWidth = 2;
manualOverlayCtx.beginPath();
manualOverlayCtx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for (let i = 1; i < polygonPoints.length; i++) {
manualOverlayCtx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
if (polygonPoints.length >= 3) {
manualOverlayCtx.closePath();
manualOverlayCtx.fill();
}
manualOverlayCtx.stroke();
polygonPoints.forEach((point, index) => {
const selected = isSelectedPoint('outer', null, index);
drawPoint(point, selected ? 'rgba(245, 158, 11, 1)' : 'rgba(22, 163, 74, 1)', selected ? 7 : 4);
if (selected) {
drawPointRing(point, 'rgba(245, 158, 11, 1)', 11);
}
});
}
function drawHoles() {
holes.forEach((hole, holeIndex) => {
drawHolePolygon(hole, true, holeIndex, 'hole');
});
if (currentHolePoints.length > 0) {
drawHolePolygon(currentHolePoints, false, null, 'currentHole');
}
}
function drawHolePolygon(points, closed, holeIndex = null, type = 'hole') {
if (!points || points.length === 0) {
return;
}
manualOverlayCtx.strokeStyle = 'rgba(220, 38, 38, 0.95)';
manualOverlayCtx.fillStyle = closed ? 'rgba(220, 38, 38, 0.22)' : 'rgba(220, 38, 38, 0.08)';
manualOverlayCtx.lineWidth = 2;
manualOverlayCtx.beginPath();
manualOverlayCtx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
manualOverlayCtx.lineTo(points[i].x, points[i].y);
}
if (closed && points.length >= 3) {
manualOverlayCtx.closePath();
manualOverlayCtx.fill();
}
manualOverlayCtx.stroke();
points.forEach((point, index) => {
const selected = isSelectedPoint(type, holeIndex, index);
drawPoint(point, selected ? 'rgba(245, 158, 11, 1)' : 'rgba(220, 38, 38, 1)', selected ? 7 : 4);
if (selected) {
drawPointRing(point, 'rgba(245, 158, 11, 1)', 11);
}
});
}
function drawPoint(point, color, radius) {
manualOverlayCtx.beginPath();
manualOverlayCtx.arc(point.x, point.y, radius, 0, Math.PI * 2);
manualOverlayCtx.fillStyle = color;
manualOverlayCtx.fill();
}
function drawPointRing(point, color, radius) {
manualOverlayCtx.beginPath();
manualOverlayCtx.arc(point.x, point.y, radius, 0, Math.PI * 2);
manualOverlayCtx.strokeStyle = color;
manualOverlayCtx.lineWidth = 2;
manualOverlayCtx.stroke();
}
function markManualResultDirty() {
lastManualResult = null;
const saveBtn = document.getElementById('saveManualAreaBtn');
if (saveBtn) {
saveBtn.disabled = true;
}
}
function isSelectedPoint(type, holeIndex, pointIndex) {
if (!selectedEditPoint) {
return false;
}
return (
selectedEditPoint.type === type &&
selectedEditPoint.pointIndex === pointIndex &&
(
type !== 'hole' ||
selectedEditPoint.holeIndex === holeIndex
)
);
}
function getSelectedPointLabel(selection) {
if (!selection) {
return 'nessun punto';
}
if (selection.type === 'outer') {
return `contorno esterno, punto ${selection.pointIndex + 1}`;
}
if (selection.type === 'hole') {
return `esclusione ${selection.holeIndex + 1}, punto ${selection.pointIndex + 1}`;
}
if (selection.type === 'currentHole') {
return `esclusione attiva, punto ${selection.pointIndex + 1}`;
}
return 'punto';
}
function getEditablePointList() {
const refs = [];
polygonPoints.forEach((point, pointIndex) => {
refs.push({
type: 'outer',
holeIndex: null,
pointIndex: pointIndex,
point: point
});
});
holes.forEach((hole, holeIndex) => {
hole.forEach((point, pointIndex) => {
refs.push({
type: 'hole',
holeIndex: holeIndex,
pointIndex: pointIndex,
point: point
});
});
});
currentHolePoints.forEach((point, pointIndex) => {
refs.push({
type: 'currentHole',
holeIndex: null,
pointIndex: pointIndex,
point: point
});
});
return refs;
}
function findNearestEditablePoint(pos, radius) {
let best = null;
let bestDistance = radius;
getEditablePointList().forEach(ref => {
const d = distance(pos, ref.point);
if (d <= bestDistance) {
bestDistance = d;
best = {
type: ref.type,
holeIndex: ref.holeIndex,
pointIndex: ref.pointIndex
};
}
});
return best;
}
function getPointArrayBySelection(selection) {
if (!selection) {
return null;
}
if (selection.type === 'outer') {
return polygonPoints;
}
if (selection.type === 'hole') {
return holes[selection.holeIndex] || null;
}
if (selection.type === 'currentHole') {
return currentHolePoints;
}
return null;
}
function updateSelectedPointPosition(pos) {
const arr = getPointArrayBySelection(selectedEditPoint);
if (!arr || !selectedEditPoint) {
return;
}
arr[selectedEditPoint.pointIndex] = {
x: pos.x,
y: pos.y
};
}
function deleteSelectedPoint() {
if (!selectedEditPoint) {
Swal.fire({
icon: 'info',
title: 'Nessun punto selezionato',
text: 'Clicca prima un punto in modalità “Modifica punti”.'
});
return;
}
const arr = getPointArrayBySelection(selectedEditPoint);
if (!arr) {
selectedEditPoint = null;
redrawManualOverlay();
return;
}
if (selectedEditPoint.type === 'outer' && arr.length <= 3) {
Swal.fire({
icon: 'warning',
title: 'Non posso eliminare',
text: 'Il contorno esterno deve avere almeno 3 punti.'
});
return;
}
if (selectedEditPoint.type === 'hole' && arr.length <= 3) {
Swal.fire({
icon: 'warning',
title: 'Non posso eliminare',
text: 'Una esclusione chiusa deve avere almeno 3 punti. Usa “Annulla punto” in modalità esclusione per rimuovere lintera esclusione.'
});
return;
}
arr.splice(selectedEditPoint.pointIndex, 1);
selectedEditPoint = null;
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
setManualStatus('Punto eliminato. Ricontrolla il profilo e calcola di nuovo larea.');
}
function pointToSegmentDistance(p, a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
if (dx === 0 && dy === 0) {
return distance(p, a);
}
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy);
t = Math.max(0, Math.min(1, t));
const projection = {
x: a.x + t * dx,
y: a.y + t * dy
};
return distance(p, projection);
}
function findNearestSegment(pos, radius) {
const candidates = [];
function addSegments(points, type, holeIndex, closed) {
if (!points || points.length < 2) {
return;
}
const limit = closed ? points.length : points.length - 1;
for (let i = 0; i < limit; i++) {
const nextIndex = (i + 1) % points.length;
if (!closed && nextIndex === 0) {
continue;
}
candidates.push({
type: type,
holeIndex: holeIndex,
insertIndex: i + 1,
a: points[i],
b: points[nextIndex],
distance: pointToSegmentDistance(pos, points[i], points[nextIndex])
});
}
}
addSegments(polygonPoints, 'outer', null, polygonPoints.length >= 3);
holes.forEach((hole, holeIndex) => {
addSegments(hole, 'hole', holeIndex, hole.length >= 3);
});
addSegments(currentHolePoints, 'currentHole', null, false);
let best = null;
candidates.forEach(candidate => {
if (candidate.distance <= radius && (!best || candidate.distance < best.distance)) {
best = candidate;
}
});
return best;
}
function insertPointOnNearestSegment(pos, radius) {
const segment = findNearestSegment(pos, radius);
if (!segment) {
return false;
}
let arr = null;
if (segment.type === 'outer') {
arr = polygonPoints;
} else if (segment.type === 'hole') {
arr = holes[segment.holeIndex];
} else if (segment.type === 'currentHole') {
arr = currentHolePoints;
}
if (!arr) {
return false;
}
arr.splice(segment.insertIndex, 0, {
x: pos.x,
y: pos.y
});
selectedEditPoint = {
type: segment.type,
holeIndex: segment.holeIndex,
pointIndex: segment.insertIndex
};
return true;
}
function proposeAutoContour() {
if (!manualPdfCanvas || !manualOverlayCanvas) {
Swal.fire({
icon: 'error',
title: 'Canvas non pronto',
text: 'Apri prima il PDF nel tracciamento manuale.'
});
return;
}
if (manualCurrentView !== 'roi') {
Swal.fire({
icon: 'info',
title: 'Prima fai Zoom su ROI',
text: 'Per lautocontorno devi prima disegnare la ROI principale e cliccare “Zoom su ROI”.'
});
return;
}
if (!autoContourRoiRect || autoContourRoiRect.w <= 0 || autoContourRoiRect.h <= 0) {
Swal.fire({
icon: 'info',
title: 'Definisci ROI autocontorno',
text: 'Clicca “ROI autocontorno” e disegna un rettangolo stretto solo attorno al profilo, senza quote, testi o frecce.'
});
return;
}
sendAutoContourRoiToPython();
}
function sendAutoContourRoiToPython() {
if (!mmPerPx) {
Swal.fire({
icon: 'info',
title: 'Prima calibra la scala',
text: 'Consiglio: calibra prima una quota, poi proponi il contorno. Il contorno può essere generato anche senza scala, ma il calcolo area richiede calibrazione.'
});
}
setManualStatus('Analisi automatica del contorno sulla ROI autocontorno...');
showOverlay();
const cropCanvas = document.createElement('canvas');
const cropCtx = cropCanvas.getContext('2d');
const sx = Math.max(0, Math.floor(autoContourRoiRect.x));
const sy = Math.max(0, Math.floor(autoContourRoiRect.y));
const sw = Math.min(
manualPdfCanvas.width - sx,
Math.max(1, Math.ceil(autoContourRoiRect.w))
);
const sh = Math.min(
manualPdfCanvas.height - sy,
Math.max(1, Math.ceil(autoContourRoiRect.h))
);
cropCanvas.width = sw;
cropCanvas.height = sh;
cropCtx.drawImage(
manualPdfCanvas,
sx,
sy,
sw,
sh,
0,
0,
sw,
sh
);
cropCanvas.toBlob(function(blob) {
if (!blob) {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore immagine',
text: 'Impossibile generare il crop della ROI autocontorno.'
});
return;
}
const formData = new FormData();
formData.append('image', blob, 'auto_contour_roi.png');
formData.append('max_points', '90');
fetch('http://127.0.0.1:5055/auto-contour-image', {
method: 'POST',
body: formData
})
.then(async response => {
const text = await response.text();
try {
return JSON.parse(text);
} catch (e) {
console.error('Risposta non JSON da Python auto-contour:', text);
throw new Error(
'Il servizio Python non ha restituito JSON. Prima parte risposta: ' +
text.substring(0, 180)
);
}
})
.then(data => {
hideOverlay();
if (!data.success) {
Swal.fire({
icon: 'warning',
title: 'Contorno non trovato',
text: data.message || 'Python non è riuscito a proporre un contorno affidabile.'
});
setManualStatus('Contorno automatico non trovato. Prova una ROI autocontorno più stretta/pulita oppure procedi manualmente.');
return;
}
if (!Array.isArray(data.outer_polygon) || data.outer_polygon.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Contorno non valido',
text: 'Il contorno proposto non contiene abbastanza punti.'
});
setManualStatus('Contorno automatico non valido. Procedi con il tracciamento manuale.');
return;
}
applyAutoContourProposal(data);
})
.catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore auto-contorno',
text: error.message || 'Errore durante la proposta del contorno.'
});
setManualStatus('Errore durante lauto-contorno. Procedi manualmente.');
});
}, 'image/png');
}
function applyAutoContourProposal(data) {
const previousPolygon = polygonPoints.map(p => ({
x: p.x,
y: p.y
}));
const previousSelected = selectedEditPoint ? {
...selectedEditPoint
} : null;
const baseRect = autoContourRoiRect || {
x: 0,
y: 0,
w: manualOverlayCanvas.width,
h: manualOverlayCanvas.height
};
const proposedPolygon = data.outer_polygon.map(point => ({
x: baseRect.x + point.x * baseRect.w,
y: baseRect.y + point.y * baseRect.h
}));
polygonPoints = proposedPolygon;
selectedEditPoint = null;
currentHolePoints = [];
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
redrawManualOverlay();
updateManualPreview();
const diagnostics = data.diagnostics || {};
const pointsCount = polygonPoints.length;
const method = diagnostics.method || 'opencv';
Swal.fire({
icon: 'question',
title: 'Contorno proposto',
html: `Ho trovato un possibile contorno esterno con <strong>${pointsCount}</strong> punti.<br>` +
`Metodo: <strong>${escapeHtml(String(method))}</strong><br><br>` +
`Vuoi usarlo come profilo esterno e poi modificarlo con “Modifica punti”?`,
showCancelButton: true,
confirmButtonText: 'Sì, usa questo contorno',
cancelButtonText: 'No, annulla'
}).then(result => {
if (!result.isConfirmed) {
polygonPoints = previousPolygon;
selectedEditPoint = previousSelected;
redrawManualOverlay();
updateManualPreview();
setManualStatus('Proposta contorno annullata. Puoi tracciare manualmente o riprovare.');
return;
}
markManualResultDirty();
setTool('edit');
setManualStatus('Contorno automatico caricato. Usa “Modifica punti” per spostare/eliminare/inserire punti, poi calcola larea.');
});
}
function calculateManualArea() {
if (!mmPerPx || !calibrationMm || !calibrationPx) {
Swal.fire({
icon: 'warning',
title: 'Calibrazione mancante',
text: 'Prima calibra una quota reale.'
});
return;
}
if (polygonPoints.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Profilo incompleto',
text: 'Servono almeno 3 punti per calcolare larea esterna.'
});
return;
}
if (currentHolePoints.length > 0) {
Swal.fire({
icon: 'warning',
title: 'Esclusione non chiusa',
text: 'Hai iniziato unarea da escludere ma non lhai chiusa. Clicca “Chiudi esclusione” oppure annulla i punti.'
});
return;
}
const outerAreaPx2 = polygonAreaPx2(polygonPoints);
let holesAreaPx2 = 0;
holes.forEach(hole => {
if (hole.length >= 3) {
holesAreaPx2 += polygonAreaPx2(hole);
}
});
const finalAreaPx2 = outerAreaPx2 - holesAreaPx2;
if (finalAreaPx2 <= 0) {
Swal.fire({
icon: 'error',
title: 'Area non valida',
text: 'Le aree da escludere sono maggiori o uguali allarea esterna.'
});
return;
}
const outerAreaMm2 = outerAreaPx2 * mmPerPx * mmPerPx;
const holesAreaMm2 = holesAreaPx2 * mmPerPx * mmPerPx;
const finalAreaMm2 = finalAreaPx2 * mmPerPx * mmPerPx;
const finalAreaCm2 = finalAreaMm2 / 100;
const bbox = polygonBoundingBox(polygonPoints);
const widthMm = bbox.width * mmPerPx;
const heightMm = bbox.height * mmPerPx;
lastManualResult = {
outer_area_px2: outerAreaPx2,
holes_area_px2: holesAreaPx2,
final_area_px2: finalAreaPx2,
outer_area_mm2: outerAreaMm2,
holes_area_mm2: holesAreaMm2,
area_mm2: finalAreaMm2,
area_cm2: finalAreaCm2,
width_mm: widthMm,
height_mm: heightMm
};
document.getElementById('saveManualAreaBtn').disabled = false;
setManualStatus(
`Area finale: ${finalAreaMm2.toFixed(3)} mm² = ${finalAreaCm2.toFixed(4)} cm². ` +
`Esterna: ${outerAreaMm2.toFixed(3)} mm² - Esclusioni: ${holesAreaMm2.toFixed(3)} mm².`
);
document.getElementById('manualCoordsPreview').innerText =
`Area finale: ${finalAreaMm2.toFixed(3)} mm² | Esclusioni: ${holes.length} | Calibrazione: ${mmPerPx.toFixed(6)} mm/px`;
}
function saveManualArea() {
if (!lastManualResult) {
Swal.fire({
icon: 'warning',
title: 'Area non calcolata',
text: 'Prima calcola larea.'
});
return;
}
const payload = {
id: currentManualJobId,
area_mm2: +lastManualResult.area_mm2.toFixed(6),
area_cm2: +lastManualResult.area_cm2.toFixed(6),
manual_outer_area_mm2: +lastManualResult.outer_area_mm2.toFixed(6),
manual_holes_area_mm2: +lastManualResult.holes_area_mm2.toFixed(6),
width_mm: +lastManualResult.width_mm.toFixed(6),
height_mm: +lastManualResult.height_mm.toFixed(6),
manual_calibration_px: +calibrationPx.toFixed(6),
manual_calibration_mm: +calibrationMm.toFixed(6),
manual_mm_per_px: +mmPerPx.toFixed(10),
manual_polygon: normalizePoints(polygonPoints),
manual_holes: holes.map(hole => normalizePoints(hole)),
calibration: normalizePoints(calibrationPoints),
roi: getNormalizedRoi(),
canvas: {
width: manualOverlayCanvas.width,
height: manualOverlayCanvas.height
}
};
showOverlay();
fetch('cad_area_save_manual_area.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(async response => {
const text = await response.text();
try {
return JSON.parse(text);
} catch (e) {
console.error('Risposta non JSON:', text);
throw new Error(
'Il server non ha restituito JSON. Prima parte risposta: ' +
text.substring(0, 180)
);
}
})
.then(data => {
hideOverlay();
if (!data.success) {
Swal.fire({
icon: 'error',
title: 'Errore salvataggio',
text: data.message || 'Impossibile salvare area manuale.'
});
return;
}
Swal.fire({
icon: 'success',
title: 'Area salvata',
html: `Area finale: <strong>${parseFloat(data.area_mm2).toFixed(3)} mm²</strong><br>` +
`Area esclusioni: <strong>${parseFloat(data.holes_area_mm2 || 0).toFixed(3)} mm²</strong>`
}).then(() => {
location.reload();
});
})
.catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore',
text: error.message || 'Errore durante il salvataggio.'
});
});
}
function polygonAreaPx2(points) {
let area = 0;
for (let i = 0; i < points.length; i++) {
const j = (i + 1) % points.length;
area += points[i].x * points[j].y;
area -= points[j].x * points[i].y;
}
return Math.abs(area / 2);
}
function polygonBoundingBox(points) {
const xs = points.map(p => p.x);
const ys = points.map(p => p.y);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
}
function normalizePoints(points) {
return points.map(point => ({
x: +(point.x / manualOverlayCanvas.width).toFixed(8),
y: +(point.y / manualOverlayCanvas.height).toFixed(8)
}));
}
function getNormalizedRoi() {
if (manualCurrentView === 'roi' && manualRoiPageRect && manualPdfPage) {
const viewportOriginal = manualPdfPage.getViewport({
scale: 1
});
return {
x: +(manualRoiPageRect.x / viewportOriginal.width).toFixed(8),
y: +(manualRoiPageRect.y / viewportOriginal.height).toFixed(8),
width: +(manualRoiPageRect.w / viewportOriginal.width).toFixed(8),
height: +(manualRoiPageRect.h / viewportOriginal.height).toFixed(8)
};
}
if (!roiRect) {
return null;
}
return {
x: +(roiRect.x / manualOverlayCanvas.width).toFixed(8),
y: +(roiRect.y / manualOverlayCanvas.height).toFixed(8),
width: +(roiRect.w / manualOverlayCanvas.width).toFixed(8),
height: +(roiRect.h / manualOverlayCanvas.height).toFixed(8)
};
}
function updateManualPreview() {
const parts = [];
if (manualCurrentView === 'roi') {
parts.push('Vista ROI ingrandita');
} else if (roiRect) {
parts.push(`ROI: ${Math.round(roiRect.w)}x${Math.round(roiRect.h)} px`);
}
if (autoContourRoiRect) {
parts.push(`ROI autocontorno: ${Math.round(autoContourRoiRect.w)}x${Math.round(autoContourRoiRect.h)} px`);
}
if (mmPerPx) {
parts.push(`Scala: ${mmPerPx.toFixed(6)} mm/px`);
}
if (polygonPoints.length > 0) {
parts.push(`Punti esterno: ${polygonPoints.length}`);
}
if (holes.length > 0) {
parts.push(`Esclusioni chiuse: ${holes.length}`);
}
if (currentHolePoints.length > 0) {
parts.push(`Punti esclusione attiva: ${currentHolePoints.length}`);
}
if (selectedEditPoint) {
parts.push(`Selezionato: ${getSelectedPointLabel(selectedEditPoint)}`);
}
document.getElementById('manualCoordsPreview').innerText =
parts.length ? parts.join(' | ') : 'Nessun dato calcolato.';
}
function setTool(tool) {
currentTool = tool;
document.getElementById('toolRoiBtn').classList.remove('active');
document.getElementById('toolCalibrationBtn').classList.remove('active');
document.getElementById('toolPolygonBtn').classList.remove('active');
const autoRoiBtn = document.getElementById('toolAutoContourRoiBtn');
if (autoRoiBtn) {
autoRoiBtn.classList.remove('active');
}
const holeBtn = document.getElementById('toolHoleBtn');
if (holeBtn) {
holeBtn.classList.remove('active');
}
const editBtn = document.getElementById('toolEditBtn');
if (editBtn) {
editBtn.classList.remove('active');
}
if (tool !== 'edit') {
selectedEditPoint = null;
isDraggingEditPoint = false;
editDragMoved = false;
}
if (tool === 'roi') {
document.getElementById('toolRoiBtn').classList.add('active');
if (manualCurrentView === 'roi') {
setManualStatus('Vista ROI ingrandita: puoi calibrare una quota o disegnare una sotto-ROI se necessario.');
} else {
setManualStatus('Modalità ROI: disegna un rettangolo intorno alla sezione.');
}
}
if (tool === 'calibration') {
document.getElementById('toolCalibrationBtn').classList.add('active');
setManualStatus('Modalità calibrazione: clicca due punti su una quota nota, poi inserisci il valore reale in mm.');
}
if (tool === 'polygon') {
document.getElementById('toolPolygonBtn').classList.add('active');
setManualStatus('Modalità profilo esterno: clicca i punti del contorno esterno. Il poligono viene chiuso automaticamente.');
}
if (tool === 'auto_roi') {
document.getElementById('toolAutoContourRoiBtn').classList.add('active');
setManualStatus('Modalità ROI autocontorno: disegna un rettangolo stretto solo attorno al profilo, escludendo quote, testi e frecce.');
}
if (tool === 'hole') {
document.getElementById('toolHoleBtn').classList.add('active');
setManualStatus('Modalità area da escludere: clicca i punti del foro/cavità da sottrarre, poi clicca “Chiudi esclusione”.');
}
if (tool === 'edit') {
document.getElementById('toolEditBtn').classList.add('active');
setManualStatus('Modalità modifica punti: clicca e trascina un punto per spostarlo. Doppio click su un segmento per aggiungere un punto. Usa “Elimina punto” per cancellare quello selezionato.');
}
redrawManualOverlay();
updateManualPreview();
}
function setManualStatus(text) {
document.getElementById('manualStatusBox').innerText = text;
}
function getCanvasMousePosition(event, canvas) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
document.getElementById('toolRoiBtn').addEventListener('click', function() {
setTool('roi');
});
document.getElementById('toolCalibrationBtn').addEventListener('click', function() {
setTool('calibration');
});
document.getElementById('toolPolygonBtn').addEventListener('click', function() {
if (!mmPerPx) {
Swal.fire({
icon: 'info',
title: 'Prima calibra la scala',
text: 'Clicca “Calibra quota” e seleziona due punti su una quota nota.'
});
return;
}
setTool('polygon');
});
document.getElementById('undoPointBtn').addEventListener('click', function() {
if (currentTool === 'hole' && currentHolePoints.length > 0) {
currentHolePoints.pop();
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
redrawManualOverlay();
updateManualPreview();
setManualStatus('Ultimo punto dellesclusione rimosso.');
return;
}
if (holes.length > 0 && currentTool === 'hole') {
holes.pop();
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
redrawManualOverlay();
updateManualPreview();
setManualStatus('Ultima esclusione chiusa rimossa.');
return;
}
if (polygonPoints.length > 0) {
polygonPoints.pop();
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
redrawManualOverlay();
updateManualPreview();
setManualStatus('Ultimo punto del profilo esterno rimosso.');
return;
}
if (calibrationPoints.length > 0) {
calibrationPoints.pop();
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
redrawManualOverlay();
updateManualPreview();
setManualStatus('Punto di calibrazione rimosso.');
return;
}
Swal.fire({
icon: 'info',
title: 'Nessun punto da annullare',
text: 'Non ci sono punti da rimuovere.'
});
});
document.getElementById('toolEditBtn').addEventListener('click', function() {
if (polygonPoints.length < 3 && holes.length === 0 && currentHolePoints.length === 0) {
Swal.fire({
icon: 'info',
title: 'Nessun profilo da modificare',
text: 'Disegna prima il contorno esterno oppure una esclusione.'
});
return;
}
setTool('edit');
selectedEditPoint = null;
redrawManualOverlay();
});
document.getElementById('deleteSelectedPointBtn').addEventListener('click', function() {
deleteSelectedPoint();
});
document.getElementById('toolHoleBtn').addEventListener('click', function() {
if (!mmPerPx) {
Swal.fire({
icon: 'info',
title: 'Prima calibra la scala',
text: 'Clicca “Calibra quota” e seleziona due punti su una quota nota.'
});
return;
}
if (polygonPoints.length < 3) {
Swal.fire({
icon: 'info',
title: 'Prima disegna il contorno esterno',
text: 'Disegna prima il profilo esterno, poi potrai indicare le aree da escludere.'
});
return;
}
setTool('hole');
});
document.getElementById('closeHoleBtn').addEventListener('click', function() {
closeCurrentHole();
});
document.getElementById('resetManualBtn').addEventListener('click', function() {
Swal.fire({
icon: 'warning',
title: 'Reset tracciamento?',
text: 'Verranno cancellati ROI, calibrazione e profilo.',
showCancelButton: true,
confirmButtonText: 'Sì, reset',
cancelButtonText: 'Annulla'
}).then(result => {
if (!result.isConfirmed) {
return;
}
resetManualState();
redrawManualOverlay();
});
});
document.getElementById('zoomRoiBtn').addEventListener('click', function() {
renderManualRoiViewFromCurrentRoi();
});
document.getElementById('toolAutoContourRoiBtn').addEventListener('click', function() {
if (manualCurrentView !== 'roi') {
Swal.fire({
icon: 'info',
title: 'Prima fai Zoom su ROI',
text: 'La ROI autocontorno va disegnata dentro la vista ingrandita della sezione.'
});
return;
}
setTool('auto_roi');
});
document.getElementById('autoContourBtn').addEventListener('click', function() {
proposeAutoContour();
});
document.getElementById('fullPageBtn').addEventListener('click', function() {
Swal.fire({
icon: 'warning',
title: 'Tornare alla pagina intera?',
text: 'Il tracciamento corrente verrà cancellato perché cambiano le coordinate del canvas.',
showCancelButton: true,
confirmButtonText: 'Sì, torna alla pagina intera',
cancelButtonText: 'Annulla'
}).then(result => {
if (!result.isConfirmed) {
return;
}
showOverlay();
renderManualFullPage(true).then(() => {
hideOverlay();
setTool('roi');
setManualStatus('Pagina intera ripristinata. Disegna una nuova ROI oppure ripeti il tracciamento.');
updateManualPreview();
}).catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore',
text: error.message || 'Impossibile tornare alla pagina intera.'
});
});
});
});
document.getElementById('calculateManualBtn').addEventListener('click', function() {
calculateManualArea();
});
document.getElementById('saveManualAreaBtn').addEventListener('click', function() {
saveManualArea();
});
function showOverlay() {
$('#processingOverlay').css('display', 'flex');
}
function hideOverlay() {
$('#processingOverlay').hide();
}
function escapeHtml(text) {
return text.replace(/[&<>"']/g, function(match) {
return ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
})[match];
});
}
</script>
</body>
</html>