1834 lines
62 KiB
PHP
1834 lines
62 KiB
PHP
<?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 l’area.',
|
||
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 un’area 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 l’area esterna.'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (currentHolePoints.length > 0) {
|
||
Swal.fire({
|
||
icon: 'warning',
|
||
title: 'Esclusione non chiusa',
|
||
text: 'Hai iniziato un’area da escludere ma non l’hai 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 all’area 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 l’area.'
|
||
});
|
||
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 dell’esclusione 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 ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
})[match];
|
||
});
|
||
}
|
||
</script>
|
||
|
||
</body>
|
||
|
||
</html>
|