Untitled
unknown
plain_text
9 months ago
43 kB
7
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