Files
zibo-dashboard/public/userarea/cad-area.php
T
2026-06-16 09:23:40 +02:00

1834 lines
62 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;
}
</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="undoPointBtn" class="btn btn-outline-secondary">
↩️ Annulla 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="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 calibrationPoints = [];
let calibrationMm = null;
let calibrationPx = null;
let mmPerPx = null;
let polygonPoints = [];
let currentHolePoints = [];
let holes = [];
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;
calibrationPoints = [];
calibrationMm = null;
calibrationPx = null;
mmPerPx = null;
polygonPoints = [];
currentHolePoints = [];
holes = [];
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;
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
};
}
};
manualOverlayCanvas.onmousemove = function(e) {
if (currentTool !== 'roi' || !isDrawingRoi) {
return;
}
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
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();
};
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 clicca “Calibra quota” e seleziona due punti su una quota nota.');
}
}
};
manualOverlayCanvas.onclick = function(e) {
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
if (currentTool === 'calibration') {
handleCalibrationClick(pos);
return;
}
if (currentTool === 'polygon') {
handlePolygonClick(pos);
return;
}
if (currentTool === 'hole') {
handleHoleClick(pos);
return;
}
};
}
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);
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);
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
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 = [];
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
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();
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 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 => {
drawPoint(point, 'rgba(22, 163, 74, 1)', 4);
});
}
function drawHoles() {
holes.forEach(hole => {
drawHolePolygon(hole, true);
});
if (currentHolePoints.length > 0) {
drawHolePolygon(currentHolePoints, false);
}
}
function drawHolePolygon(points, closed) {
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 => {
drawPoint(point, 'rgba(220, 38, 38, 1)', 4);
});
}
function drawPoint(point, color, radius) {
manualOverlayCtx.beginPath();
manualOverlayCtx.arc(point.x, point.y, radius, 0, Math.PI * 2);
manualOverlayCtx.fillStyle = color;
manualOverlayCtx.fill();
}
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 (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}`);
}
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 holeBtn = document.getElementById('toolHoleBtn');
if (holeBtn) {
holeBtn.classList.remove('active');
}
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 === 'hole') {
document.getElementById('toolHoleBtn').classList.add('active');
setManualStatus('Modalità area da escludere: clicca i punti del foro/cavità da sottrarre, poi clicca “Chiudi esclusione”.');
}
}
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('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('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>