Untitled
unknown
plain_text
23 days ago
43 kB
2
Indexable
{% extends "base.html" %} {% load static %} {% block title %}Takeoff Screen{% endblock %} {% block content %} <div class="takeoff-container"> <div class="takeoff-content"> <!-- Sidebar --> <div class="takeoff-sidebar"> <!-- Project Selector --> <div class="sidebar-section"> <label for="project-selector" class="form-label small">Project:</label> <select id="project-selector" class="form-select form-select-sm" onchange="changeProject(this.value)"> <option value="{{ project.id }}" selected>{{ project.name }}</option> {% for proj in all_projects %} {% if proj.id != project.id %} <option value="{{ proj.id }}">{{ proj.name }}</option> {% endif %} {% endfor %} </select> </div> <!-- PDF Selector --> <div class="sidebar-section"> <label for="pdf-selector" class="form-label small">PDF Document:</label> <select id="pdf-selector" class="form-select form-select-sm" onchange="changePDF(this.value)"> {% for pdf in pdfs %} <option value="{{ pdf.id }}" data-url="{{ pdf.file.url }}">{{ pdf.file.name|slice:"6:" }}</option> {% empty %} <option disabled>No PDFs available</option> {% endfor %} </select> <div class="d-grid gap-2 mt-1"> <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#uploadPdfModal">Upload PDF</button> </div> </div> <!-- Scale Calibration Button --> <div class="sidebar-section"> <button class="btn btn-outline-secondary btn-sm w-100" onclick="toggleScaleBox()">Set/Calibrate Scale</button> </div> <!-- Measurement Tools --> <div class="sidebar-section"> <h6 class="small fw-bold">Measurement Tools</h6> <div class="d-grid gap-1"> <button class="btn btn-sm btn-outline-dark" onclick="selectTool('lineal')">Lineal (m)</button> <button class="btn btn-sm btn-outline-dark" onclick="selectTool('area')">Area (m²)</button> <button class="btn btn-sm btn-outline-dark" onclick="selectTool('volume')">Volume (m³)</button> <button class="btn btn-sm btn-outline-dark" onclick="selectTool('count')">Count (ea)</button> </div> <div class="form-check mt-2"> <input class="form-check-input" type="checkbox" value="" id="deduction-check"> <label class="form-check-label small" for="deduction-check"> Deduction </label> </div> </div> <!-- Measurements List --> <div class="sidebar-section measurements-container"> <h6 class="small fw-bold">Measurements</h6> <table class="table table-sm small"> <thead> <tr> <th>Type</th> <th>Value</th> <th></th> </tr> </thead> <tbody id="measurements-table"> <!-- Measurements will be added here dynamically --> </tbody> </table> <div class="d-flex justify-content-between align-items-center mt-3"> <button type="button" class="btn btn-primary btn-sm" onclick="saveMeasurementsAndReturn()">Save and Return to Estimating</button> </div> </div> </div> <!-- Main Canvas Area --> <div class="takeoff-canvas-container"> <!-- PDF Controls --> <div class="pdf-controls"> <div class="btn-group btn-group-sm me-2"> <button class="btn btn-outline-secondary" onclick="zoomOut()"><i class="fas fa-search-minus"></i></button> <button class="btn btn-outline-secondary" onclick="resetZoom()">100%</button> <button class="btn btn-outline-secondary" onclick="zoomIn()"><i class="fas fa-search-plus"></i></button> </div> <div class="btn-group btn-group-sm"> <button class="btn btn-outline-secondary" onclick="fitToWidth()">Fit Width</button> <button class="btn btn-outline-secondary" onclick="fitToPage()">Fit Page</button> </div> <span class="ms-2 zoom-level">Zoom: 100%</span> <div class="ms-3"> <span class="small">Page: </span> <select id="page-selector" class="form-select form-select-sm d-inline-block" style="width: auto;" onchange="changePage(this.value)"> <!-- Page options will be added dynamically --> </select> </div> </div> <!-- Image Viewer --> <div id="pdf-container"> <div id="image-render" class="position-relative"> <!-- Loading spinner --> <div id="loading-spinner" class="position-absolute top-50 start-50 translate-middle"> <div class="spinner-border text-primary" role="status"> <span class="visually-hidden">Loading...</span> </div> </div> </div> </div> <!-- Floating Scale Calibration Box --> <div id="scale-calibration" class="position-absolute top-0 start-0 bg-light p-2 m-2 rounded shadow-sm collapse"> <h6>Set/Calibrate Scale</h6> <form method="post" id="scale-form" class="d-flex flex-column"> {% csrf_token %} <input type="hidden" name="pdf_id" id="scale-pdf-id"> <div class="mb-2"> <label for="scale-ratio" class="form-label small">Scale Ratio:</label> <select id="scale-ratio" class="form-select form-select-sm" onchange="handleScaleSelection(this.value)"> <option value="1:5">1:5</option> <option value="1:10">1:10</option> <option value="1:20">1:20</option> <option value="1:50">1:50</option> <option value="1:100" selected>1:100</option> <option value="1:200">1:200</option> <option value="custom">Custom</option> </select> </div> <div class="mb-2"> <label for="page-size" class="form-label small">Page Size:</label> <select id="page-size" class="form-select form-select-sm" onchange="handlePageSizeChange(this.value)"> <option value="A1">A1</option> <option value="A3" selected>A3</option> <option value="A4">A4</option> </select> </div> <div id="custom-scale-input" class="mb-2 collapse"> <label for="custom-scale" class="form-label small">Custom Scale:</label> <input type="number" id="custom-scale-value" class="form-control form-control-sm" min="1" step="0.01"> <button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="promptBenchmarkMeasurement()">Calculate Scale</button> </div> <div class="d-flex justify-content-between align-items-center"> <button type="button" class="btn btn-primary btn-sm" onclick="saveScale()">Save</button> <button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleScaleBox()">Close</button> </div> </form> </div> </div> </div> </div> <!-- PDF Upload Modal --> <div class="modal fade" id="uploadPdfModal" tabindex="-1" aria-labelledby="uploadPdfModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="uploadPdfModalLabel">Upload PDF</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <form method="post" enctype="multipart/form-data" id="pdf-upload-form"> {% csrf_token %} {{ pdf_upload_form.as_p }} </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-primary" onclick="document.getElementById('pdf-upload-form').submit()">Upload</button> </div> </div> </div> </div> <style> /* Main container styles */ .takeoff-container { position: absolute; top: 56px; /* Adjust based on your navbar height */ bottom: 0; left: 0; right: 0; overflow: hidden; margin-top: 1px; /* Added to fix navbar overlap */ padding-top: 20px; /* Ensure content is pushed below the navbar */ } .takeoff-content { display: flex; height: 100%; width: 100%; } /* Sidebar styles */ .takeoff-sidebar { width: 260px; min-width: 260px; /* Prevent sidebar from shrinking */ height: 100%; background-color: #f8f9fa; overflow-y: auto; display: flex; flex-direction: column; border-right: 1px solid #dee2e6; z-index: 10; } .sidebar-section { padding: 10px; border-bottom: 1px solid #dee2e6; } .measurements-container { flex-grow: 1; overflow-y: auto; } /* Canvas container */ .takeoff-canvas-container { flex-grow: 1; height: 100%; display: flex; flex-direction: column; position: relative; min-width: 0; /* Allow canvas to shrink if necessary */ } /* PDF controls */ .pdf-controls { padding: 8px 10px; background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; display: flex; align-items: center; } .zoom-level { font-size: 0.9rem; color: #6c757d; } /* PDF container */ #pdf-container { flex-grow: 1; overflow: auto; position: relative; background-color: #e0e0e0; display: flex; align-items: flex-start; justify-content: flex-start; /* Changed from center to flex-start */ padding: 20px; } #image-render { background-color: white; box-shadow: 0 0 10px rgba(0,0,0,0.2); margin: 0; /* Removed auto margin */ position: relative; transform-origin: top left; } #image-render img { display: block; /* Remove any extra space below image */ max-width: none; /* Prevent Bootstrap from restricting the image size */ } #image-render canvas { position: absolute; top: 0; left: 0; pointer-events: none; /* Let events pass through to the image */ } /* Style for the floating scale box */ #scale-calibration { z-index: 1000; max-width: 250px; } </style> <script> let currentPDFId = null; let currentPageIndex = 0; let currentTool = 'lineal'; let isDeduction = false; let measurements = []; let currentScale = 1.0; let pdfImages = []; // Array to store image URLs for each page let aspectRatio = 1.414; // Default A4 aspect ratio (width:height = 1:1.414) let originalImageSize = { width: 0, height: 0 }; // Store original image dimensions let drawingLayer = null; // Canvas for drawing measurements let isCalibrationMode = false; let calibrationLineStart = null; let calibrationLineEnd = null; let currentScaleRatio = 100; // Default scale 1:100 (1mm = 100mm) let currentPageSize = 'A3'; // Default page size let pageSizeMmDimensions = { 'A1': { width: 841, height: 594 }, 'A3': { width: 420, height: 297 }, 'A4': { width: 297, height: 210 } }; // Initialize on page load document.addEventListener('DOMContentLoaded', function() { // Make "lineal" measurement active by default document.querySelector('.btn-outline-dark').classList.add('active'); // Get the first PDF if available const pdfSelector = document.getElementById('pdf-selector'); if (pdfSelector && pdfSelector.options.length > 0) { const selectedOption = pdfSelector.options[0]; currentPDFId = selectedOption.value; loadPDFImages(currentPDFId); } // Set up event listener for deduction checkbox document.getElementById('deduction-check').addEventListener('change', function() { isDeduction = this.checked; }); // Set up scroll wheel zoom with Ctrl key const pdfContainer = document.getElementById('pdf-container'); pdfContainer.addEventListener('wheel', function(e) { // Only zoom if Ctrl key is pressed if (e.ctrlKey) { e.preventDefault(); // Prevent default scroll behavior // Calculate new scale based on scroll direction const delta = e.deltaY || e.detail || e.wheelDelta; if (delta > 0) { // Zoom out - smaller step for smoother zoom const newScale = Math.max(0.1, currentScale - 0.1); setZoom(newScale); } else { // Zoom in - smaller step for smoother zoom const newScale = Math.min(5.0, currentScale + 0.1); setZoom(newScale); } } }); }); // Function to load PDF as images function loadPDFImages(pdfId) { showLoading(true); // Call the backend to convert PDF to images fetch(`/convert-pdf-to-images/${pdfId}/`) .then(response => response.json()) .then(data => { if (data.success) { pdfImages = data.images; // Update page selector updatePageSelector(data.page_count); // Load the first page currentPageIndex = 0; loadPage(currentPageIndex); // Fetch measurements for this PDF fetchMeasurements(pdfId); // Update scale form with the current PDF ID document.getElementById('scale-pdf-id').value = pdfId; // Load saved scale if available if (data.scale_ratio) { currentScaleRatio = data.scale_ratio; console.log(`Loaded saved scale: 1:${currentScaleRatio}`); // Update the scale ratio dropdown if it matches a standard option const scaleSelect = document.getElementById('scale-ratio'); const standardScaleOptions = ['5', '10', '20', '50', '100', '200']; if (standardScaleOptions.includes(currentScaleRatio.toString())) { scaleSelect.value = `1:${currentScaleRatio}`; } else { scaleSelect.value = 'custom'; handleScaleSelection('custom'); const customScaleInput = document.getElementById('custom-scale'); if (customScaleInput) { customScaleInput.value = currentScaleRatio; } } } else { // Default to 1:100 for newly uploaded PDFs currentScaleRatio = 100; } } else { // Display the specific error from the server if (data.error && data.error.includes('poppler-utils')) { showError('Failed to convert PDF: Missing required dependency "poppler-utils". Please install it and try again.'); } else { showError(`Failed to convert PDF: ${data.error || 'Unknown error'}`); } console.error('Error details:', data.error); } }) .catch(error => { console.error('Network or parsing error:', error); showError('Network error converting PDF. Please check your connection and try again.'); }); } // Update page selector dropdown function updatePageSelector(pageCount) { const pageSelector = document.getElementById('page-selector'); pageSelector.innerHTML = ''; for (let i = 0; i < pageCount; i++) { const option = document.createElement('option'); option.value = i; option.textContent = `Page ${i + 1}`; pageSelector.appendChild(option); } } // Function to load a specific page function loadPage(pageIndex) { if (pageIndex < 0 || pageIndex >= pdfImages.length) { return; } currentPageIndex = pageIndex; // Update page selector const pageSelector = document.getElementById('page-selector'); pageSelector.value = pageIndex; showLoading(true); // Clear the image container const imageContainer = document.getElementById('image-render'); imageContainer.innerHTML = ''; // Create image element const img = new Image(); img.onload = function() { // Store original image dimensions originalImageSize = { width: img.naturalWidth, aspectRatio: img.naturalWidth / img.naturalHeight }; // Add drawing canvas on top of the image setupDrawingLayer(img); // Apply current zoom setZoom(currentScale); showLoading(false); }; img.onerror = function() { showError('Failed to load image'); }; // Set image source img.src = pdfImages[pageIndex]; img.id = 'pdf-image'; imageContainer.appendChild(img); } // Setup the drawing canvas layer function setupDrawingLayer(imageElement) { const imageContainer = document.getElementById('image-render'); // Create canvas element for drawing const canvas = document.createElement('canvas'); canvas.width = imageElement.naturalWidth; canvas.height = imageElement.naturalHeight; canvas.id = 'drawing-layer'; canvas.style.pointerEvents = 'auto'; // Enable mouse events // Add canvas to container imageContainer.appendChild(canvas); // Store the canvas context drawingLayer = canvas.getContext('2d'); // Initialize drawing on the canvas initializeDrawing(canvas); // Redraw measurements redrawMeasurements(); } // Function to change PDF function changePDF(pdfId) { if (!pdfId || pdfId === currentPDFId) return; currentPDFId = pdfId; loadPDFImages(pdfId); } // Function to change page function changePage(pageIndex) { loadPage(parseInt(pageIndex, 10)); } // Toggle scale calibration box function toggleScaleBox() { const scaleBox = document.getElementById('scale-calibration'); scaleBox.classList.toggle('collapse'); // Ensure the PDF ID is set if (currentPDFId) { document.getElementById('scale-pdf-id').value = currentPDFId; } } // Select measurement tool function selectTool(tool) { currentTool = tool; // Highlight the selected tool button const buttons = document.querySelectorAll('.btn-outline-dark'); buttons.forEach(btn => { btn.classList.remove('active'); if (btn.textContent.toLowerCase().includes(tool)) { btn.classList.add('active'); } }); } // Set zoom level function setZoom(scale) { currentScale = scale; // Apply zoom to image and canvas const imageRender = document.getElementById('image-render'); const image = document.getElementById('pdf-image'); const canvas = document.getElementById('drawing-layer'); if (image && canvas) { // Calculate new dimensions const newWidth = image.naturalWidth * currentScale; // Apply zoom image.style.width = `${newWidth}px`; canvas.style.width = `${newWidth}px`; canvas.style.height = 'auto'; // Update zoom display updateZoomDisplay(); // Redraw measurements at the new scale redrawMeasurements(); } } // Update zoom level display function updateZoomDisplay() { document.querySelector('.zoom-level').textContent = `Zoom: ${Math.round(currentScale * 100)}%`; } // Zoom control functions function zoomIn() { setZoom(Math.min(5.0, currentScale + 0.1)); } function zoomOut() { setZoom(Math.max(0.1, currentScale - 0.1)); } function resetZoom() { setZoom(1.0); } function fitToWidth() { const pdfContainer = document.getElementById('pdf-container'); const containerWidth = pdfContainer.clientWidth - 40; // Account for padding if (originalImageSize.width) { const newScale = containerWidth / originalImageSize.width; setZoom(newScale); } } function fitToPage() { const pdfContainer = document.getElementById('pdf-container'); const containerWidth = pdfContainer.clientWidth - 40; const containerHeight = pdfContainer.clientHeight - 40; if (originalImageSize.width) { const imageHeight = originalImageSize.width / originalImageSize.aspectRatio; // Get scale based on width and height const scaleWidth = containerWidth / originalImageSize.width; const scaleHeight = containerHeight / imageHeight; // Choose the smaller scale to ensure the entire page fits setZoom(Math.min(scaleWidth, scaleHeight)); } } // Show/hide loading spinner function showLoading(show) { const spinner = document.getElementById('loading-spinner'); if (spinner) { spinner.style.display = show ? 'block' : 'none'; } } // Show error message function showError(message) { showLoading(false); const imageRender = document.getElementById('image-render'); imageRender.innerHTML = `<div class="alert alert-danger m-3">${message}</div>`; } // Initialize drawing on canvas function initializeDrawing(canvas) { let isDrawing = false; let startX, startY, endX, endY; // Mouse down event canvas.addEventListener('mousedown', function(e) { const rect = canvas.getBoundingClientRect(); startX = (e.clientX - rect.left) * (canvas.width / rect.width); startY = (e.clientY - rect.top) * (canvas.height / rect.height); isDrawing = true; if (isCalibrationMode) { calibrationLineStart = { x: startX, y: startY }; } }); // Mouse move event canvas.addEventListener('mousemove', function(e) { if (!isDrawing) return; const rect = canvas.getBoundingClientRect(); endX = (e.clientX - rect.left) * (canvas.width / rect.width); endY = (e.clientY - rect.top) * (canvas.height / rect.height); // Clear and redraw drawingLayer.clearRect(0, 0, canvas.width, canvas.height); // Draw temporary measurement drawMeasurement(drawingLayer, startX, startY, endX, endY, currentTool, true); // Redraw existing measurements measurements.forEach(m => { if (m.coordinates) { const coords = m.coordinates; // Convert from normalized to absolute coordinates const startX = coords.startX * canvas.width; const startY = coords.startY * canvas.height; const endX = coords.endX * canvas.width; const endY = coords.endY * canvas.height; drawMeasurement( drawingLayer, startX, startY, endX, endY, m.type, false, m.is_deduction ); } }); }); // Mouse up event canvas.addEventListener('mouseup', async function(e) { if (!isDrawing) return; isDrawing = false; const rect = canvas.getBoundingClientRect(); endX = (e.clientX - rect.left) * (canvas.width / rect.width); endY = (e.clientY - rect.top) * (canvas.height / rect.height); if (isCalibrationMode) { calibrationLineEnd = { x: endX, y: endY }; const pixelLength = calculateMeasurement( calibrationLineStart.x, calibrationLineStart.y, calibrationLineEnd.x, calibrationLineEnd.y, 'lineal' ); const actualLength = prompt('Enter the actual length of the line you drew (in mm):'); if (actualLength) { calculateScaleFromBenchmark(pixelLength, actualLength); } isCalibrationMode = false; enableMeasurementTools(); return; } // Calculate the measurement based on tool type and scale const value = calculateMeasurement(startX, startY, endX, endY, currentTool); // Add to measurements array const measurementData = { type: currentTool, value: parseFloat(value.toFixed(2)), is_deduction: isDeduction, coordinates: { startX: startX / canvas.width, startY: startY / canvas.height, endX: endX / canvas.width, endY: endY / canvas.height } }; // Save to server const savedMeasurement = await saveMeasurement(measurementData); if (savedMeasurement) { // Add the saved measurement to our local array measurements.push({ id: savedMeasurement.id, type: savedMeasurement.type, value: savedMeasurement.value, is_deduction: savedMeasurement.is_deduction, coordinates: savedMeasurement.coordinates }); // Update measurements table updateMeasurementsTable(); } // Redraw everything drawingLayer.clearRect(0, 0, canvas.width, canvas.height); redrawMeasurements(); }); } // Draw measurement based on tool type function drawMeasurement(context, startX, startY, endX, endY, tool, isTemporary, isDeduct) { const deductionToUse = isDeduct !== undefined ? isDeduct : isDeduction; context.beginPath(); context.strokeStyle = deductionToUse ? 'red' : 'blue'; context.lineWidth = 2; switch(tool) { case 'lineal': // Draw a line context.moveTo(startX, startY); context.lineTo(endX, endY); break; case 'area': // Draw a rectangle context.rect(startX, startY, endX - startX, endY - startY); if (!isTemporary) { context.fillStyle = deductionToUse ? 'rgba(255,0,0,0.2)' : 'rgba(0,0,255,0.2)'; context.fill(); } break; case 'volume': // Draw a 3D-like box (simplified) const offset = 20; context.moveTo(startX, startY); context.lineTo(endX, endY); context.moveTo(startX, startY); context.lineTo(startX + offset, startY - offset); context.lineTo(endX + offset, endY - offset); context.lineTo(endX, endY); context.closePath(); if (!isTemporary) { context.fillStyle = deductionToUse ? 'rgba(255,0,0,0.2)' : 'rgba(0,0,255,0.2)'; context.fill(); } break; case 'count': // Draw a circle const radius = 10; context.arc(startX, startY, radius, 0, 2 * Math.PI); if (!isTemporary) { context.fillStyle = deductionToUse ? 'rgba(255,0,0,0.7)' : 'rgba(0,0,255,0.7)'; context.fill(); } break; } context.stroke(); // Add label if not temporary if (!isTemporary) { const measurement = calculateMeasurement(startX, startY, endX, endY, tool); context.font = '28px Arial'; context.fillStyle = 'black'; context.fillText(`${measurement.toFixed(2)} ${getMeasurementUnit(tool)}`, (startX + endX) / 2, (startY + endY) / 2); } } // Calculate measurement based on tool type, coordinates, and scale function calculateMeasurement(startX, startY, endX, endY, tool) { // Get pixel measurements let pixelMeasurement; switch(tool) { case 'lineal': // Distance formula pixelMeasurement = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); break; case 'area': // Area of rectangle pixelMeasurement = Math.abs((endX - startX) * (endY - startY)); break; case 'volume': // Volume of box (with assumed depth) const depth = 20; // Example assumed depth in pixels pixelMeasurement = Math.abs((endX - startX) * (endY - startY) * depth); break; case 'count': // Just return 1 for count (no scaling needed) return 1; default: return 0; } // Get the pixel-to-mm conversion factor based on the current page size const pixelToMm = getPixelToMmConversionFactor(); // Convert pixel measurement to mm then apply scale let actualMeasurement; switch(tool) { case 'lineal': // Convert to meters with scale actualMeasurement = (pixelMeasurement * pixelToMm * currentScaleRatio) / 1000; break; case 'area': // Convert to square meters with scale actualMeasurement = (pixelMeasurement * Math.pow(pixelToMm * currentScaleRatio, 2)) / 1000000; break; case 'volume': // Convert to cubic meters with scale actualMeasurement = (pixelMeasurement * Math.pow(pixelToMm * currentScaleRatio, 3)) / 1000000000; break; default: actualMeasurement = 0; } return actualMeasurement; } // Get unit based on measurement type function getMeasurementUnit(tool) { switch(tool) { case 'lineal': return 'm'; case 'area': return 'm²'; case 'volume': return 'm³'; case 'count': return 'ea'; default: return ''; } } // Update the measurements table function updateMeasurementsTable() { const tbody = document.getElementById('measurements-table'); tbody.innerHTML = ''; if (!measurements || measurements.length === 0) { const row = document.createElement('tr'); row.innerHTML = '<td colspan="3" class="text-center">No measurements yet</td>'; tbody.appendChild(row); return; } measurements.forEach(m => { const row = document.createElement('tr'); row.innerHTML = ` <td>${m.type} ${m.is_deduction ? '(-)' : ''}</td> <td>${typeof m.value === 'number' ? m.value.toFixed(2) : m.value} ${getMeasurementUnit(m.type)}</td> <td><button class="btn btn-sm btn-danger" onclick="deleteMeasurement(${m.id})">×</button></td> `; tbody.appendChild(row); }); } // Delete a measurement function deleteMeasurement(id) { // Find the measurement to delete const measurementToDelete = measurements.find(m => m.id === id); if (!measurementToDelete) return; // Confirm deletion if (!confirm("Are you sure you want to delete this measurement?")) { return; } // Remove from array measurements = measurements.filter(m => m.id !== id); // Update the table updateMeasurementsTable(); // Redraw remaining measurements if (drawingLayer) { drawingLayer.clearRect(0, 0, drawingLayer.canvas.width, drawingLayer.canvas.height); redrawMeasurements(); } // Delete from server fetch(`/takeoff-screen/${getProjectId()}/delete-measurement/${id}/`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken(), } }).catch(error => console.error('Error deleting measurement:', error)); } // Fetch existing measurements function fetchMeasurements(pdfId) { if (!pdfId) return; fetch(`/takeoff-screen/${getProjectId()}/measurements/${pdfId}/`) .then(response => { if (!response.ok) { throw new Error('Failed to fetch measurements'); } return response.json(); }) .then(data => { measurements = data.measurements || []; updateMeasurementsTable(); redrawMeasurements(); }) .catch(error => { console.error('Error fetching measurements:', error); }); } // Save a measurement to the database async function saveMeasurement(measurementData) { try { const response = await fetch(`/takeoff-screen/${getProjectId()}/save-measurement/`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken(), }, body: JSON.stringify({ pdf_id: currentPDFId, type: measurementData.type, value: measurementData.value, is_deduction: measurementData.is_deduction, coordinates: measurementData.coordinates }) }); if (!response.ok) { throw new Error('Failed to save measurement'); } const data = await response.json(); return data; } catch (error) { console.error('Error saving measurement:', error); return null; } } // Function to change project function changeProject(projectId) { if (projectId) { window.location.href = `/takeoff-screen/${projectId}/`; } } // Get CSRF token from cookie function getCsrfToken() { const name = 'csrftoken'; let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } // Get the project ID from the URL function getProjectId() { const pathParts = window.location.pathname.split('/'); return pathParts[2]; // Assumes URL pattern is /takeoff-screen/:projectId/ } // Redraw all measurements on the canvas function redrawMeasurements() { if (!measurements || measurements.length === 0 || !drawingLayer) return; const canvas = drawingLayer.canvas; drawingLayer.clearRect(0, 0, canvas.width, canvas.height); // Draw each measurement with its stored coordinates measurements.forEach(m => { // If we have stored coordinates, use them if (m.coordinates) { const coords = m.coordinates; // Convert from normalized to absolute coordinates const startX = coords.startX * canvas.width; const startY = coords.startY * canvas.height; const endX = coords.endX * canvas.width; const endY = coords.endY * canvas.height; drawMeasurement( drawingLayer, startX, startY, endX, endY, m.type, false, // Not temporary m.is_deduction ); } }); } // Handle window resize to adjust fit window.addEventListener('resize', function() { if (document.getElementById('pdf-image')) { fitToPage(); } }); function handleScaleSelection(value) { const customInput = document.getElementById('custom-scale-input'); if (value === 'custom') { customInput.classList.remove('collapse'); } else { customInput.classList.add('collapse'); // Apply the selected standard scale immediately const scaleValue = parseInt(value.split(':')[1]); if (!isNaN(scaleValue)) { currentScaleRatio = scaleValue; applyScaleToDrawing(scaleValue); } } } function promptBenchmarkMeasurement() { // Enter calibration mode and prompt user to draw a line isCalibrationMode = true; alert('Please draw a line on the drawing to represent a known length. After drawing, you will be prompted to enter the actual length.'); // Disable normal measurement tools during calibration disableMeasurementTools(); // Show instruction on screen const pdfContainer = document.getElementById('pdf-container'); const instructionEl = document.createElement('div'); instructionEl.id = 'calibration-instruction'; instructionEl.className = 'position-absolute top-0 start-0 bg-warning p-2 m-2 rounded'; instructionEl.textContent = 'CALIBRATION MODE: Draw a line representing a known distance'; pdfContainer.appendChild(instructionEl); } function disableMeasurementTools() { // Temporarily disable regular measurement buttons const measurementButtons = document.querySelectorAll('.btn-outline-dark'); measurementButtons.forEach(btn => { btn.disabled = true; }); } function enableMeasurementTools() { // Re-enable measurement buttons const measurementButtons = document.querySelectorAll('.btn-outline-dark'); measurementButtons.forEach(btn => { btn.disabled = false; }); // Remove instruction const instructionEl = document.getElementById('calibration-instruction'); if (instructionEl) instructionEl.remove(); } function calculateScaleFromBenchmark(drawnLineLength, actualLengthMm) { // Convert drawn line length from pixels to mm using the corrected factor const pixelToMm = getPixelToMmConversionFactor(); const drawnLineLengthMm = drawnLineLength * pixelToMm; // Calculate scale ratio (1:X) const scaleRatio = actualLengthMm / drawnLineLengthMm; // Update the UI to show the calculated scale alert(`Scale calculated: 1:${scaleRatio.toFixed(2)}`); // Apply the new scale applyScaleToDrawing(scaleRatio); return scaleRatio; } function applyScaleToDrawing(scaleRatio) { // Save the scale ratio to be applied to all measurements currentScaleRatio = scaleRatio; console.log(`Applied scale ratio: 1:${scaleRatio}`); // Update all existing measurements if needed redrawMeasurements(); // Save to server for persistence if (currentPDFId) { fetch(`/save-scale/${currentPDFId}/`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken(), }, body: JSON.stringify({ scale_ratio: scaleRatio }) }) .then(response => response.json()) .then(data => { console.log('Scale saved:', data); }) .catch(error => { console.error('Error saving scale:', error); }); } } // Function to save the scale setting without reloading the page function saveScale() { let scaleRatio; // Get the scale value from either dropdown or custom input const scaleSelect = document.getElementById('scale-ratio'); if (scaleSelect.value === 'custom') { // Get custom scale value const customInput = document.getElementById('custom-scale-value'); if (customInput && customInput.value) { scaleRatio = parseFloat(customInput.value); } else { alert('Please enter a valid custom scale value'); return; } } else { // Get selected scale from dropdown scaleRatio = parseInt(scaleSelect.value.split(':')[1]); } // Validate the scale ratio if (isNaN(scaleRatio) || scaleRatio <= 0) { alert('Please enter a valid scale ratio'); return; } // Apply and save the scale applyScaleToDrawing(scaleRatio); // Update UI alert(`Scale set to 1:${scaleRatio}`); // Close the scale box toggleScaleBox(); } // Update the pixel-to-mm conversion based on page size function getPixelToMmConversionFactor() { // Base conversion factor (72 DPI: 1 inch = 25.4 mm, so 1 pixel = 25.4/72 mm) const basePixelToMm = 0.3528; // For A3 page at 1:50 scale that's showing 2000mm when it should be 1000mm, // we need to apply a correction factor of 0.5 const correctionFactor = 0.5; return basePixelToMm * correctionFactor; } // Update scale calculations if page size or scale changes function updateScaleCalculations() { // Redraw measurements with new calculations redrawMeasurements(); } // Function to handle page size change function handlePageSizeChange(pageSize) { currentPageSize = pageSize; console.log(`Page size changed to ${pageSize}`); // Recalculate and apply scale if needed updateScaleCalculations(); } // Save measurements and return to estimating function saveMeasurementsAndReturn() { // Save measurements logic here alert('Measurements saved successfully!'); window.location.href = `/estimating-screen/${getProjectId()}/`; } </script> <script src="{% static 'myapp/js/takeoff.js' %}"></script> <div id="takeoff-canvas" data-project-id="{{ project.id }}" data-pdf-id="{{ pdf.id }}"></div> {% endblock %}
Editor is loading...
Leave a Comment