490 lines
20 KiB
HTML
490 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Wall Board Coverage Visualization</title>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
margin: 20px;
|
||
background-color: #f0f0f0;
|
||
}
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
.wall-section {
|
||
margin-bottom: 40px;
|
||
padding: 20px;
|
||
background-color: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
.canvas-container {
|
||
position: relative;
|
||
margin-top: 20px;
|
||
}
|
||
canvas {
|
||
background-color: white;
|
||
border: 1px solid #ccc;
|
||
}
|
||
#boardInfo {
|
||
position: absolute;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
display: none;
|
||
}
|
||
.legend {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
gap: 20px;
|
||
}
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.legend-color {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 1px solid #000;
|
||
}
|
||
.dimensions {
|
||
margin-top: 20px;
|
||
font-size: 14px;
|
||
}
|
||
.stats {
|
||
margin-top: 20px;
|
||
padding: 10px;
|
||
background-color: #f8f8f8;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
}
|
||
.total-summary {
|
||
margin-top: 40px;
|
||
padding: 20px;
|
||
background-color: #e8f5e9;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
h2 {
|
||
margin-top: 0;
|
||
color: #2e7d32;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>Wall Board Coverage Visualization</h1>
|
||
<div id="wallSections"></div>
|
||
<div class="legend">
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: #4CAF50;"></div>
|
||
<span>Full Boards</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: #FFA726;"></div>
|
||
<span>Cut Boards</span>
|
||
</div>
|
||
</div>
|
||
<div class="total-summary" id="totalSummary"></div>
|
||
<div id="boardInfo"></div>
|
||
</div>
|
||
|
||
<script src="visualization_data.js"></script>
|
||
<script>
|
||
console.log('Starting visualization script...');
|
||
|
||
// Add error handling and debugging for data
|
||
if (typeof wallsData === 'undefined') {
|
||
console.error('Visualization data not loaded!');
|
||
document.getElementById('wallSections').innerHTML =
|
||
'<div style="color: red; padding: 20px;">Error: Visualization data not loaded. Please run the Python script first.</div>';
|
||
} else {
|
||
console.log('Loaded wall data:', wallsData);
|
||
console.log('Number of walls:', wallsData.length);
|
||
|
||
const PADDING = 40;
|
||
const SCALE_FACTOR = 100; // pixels per meter
|
||
|
||
function drawWall(ctx, wall) {
|
||
ctx.strokeStyle = '#000';
|
||
ctx.lineWidth = 2;
|
||
|
||
if (wall.shape === 'rectangular') {
|
||
ctx.strokeRect(
|
||
PADDING,
|
||
PADDING,
|
||
wall.width * SCALE_FACTOR,
|
||
wall.height * SCALE_FACTOR
|
||
);
|
||
} else if (wall.shape === 'triangular') {
|
||
const baseWidth = wall.width * SCALE_FACTOR;
|
||
const height = wall.height * SCALE_FACTOR;
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(PADDING, PADDING + height); // Bottom left
|
||
ctx.lineTo(PADDING + baseWidth, PADDING + height); // Bottom right
|
||
ctx.lineTo(PADDING + (baseWidth / 2), PADDING); // Top middle
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
function drawWindows(ctx, wall) {
|
||
if (!wall.windows || wall.windows.length === 0) return;
|
||
|
||
// Save the current context state
|
||
ctx.save();
|
||
|
||
// Set window style
|
||
ctx.strokeStyle = '#000';
|
||
ctx.lineWidth = 2;
|
||
ctx.fillStyle = 'rgba(135, 206, 235, 0.5)'; // Light blue with 50% transparency
|
||
|
||
// Draw each window
|
||
wall.windows.forEach(window => {
|
||
const x = PADDING + (window.left * SCALE_FACTOR);
|
||
const y = PADDING + ((wall.height - window.bottom - window.height) * SCALE_FACTOR);
|
||
const width = window.width * SCALE_FACTOR;
|
||
const height = window.height * SCALE_FACTOR;
|
||
|
||
// Draw window with semi-transparent fill
|
||
ctx.fillRect(x, y, width, height);
|
||
ctx.strokeRect(x, y, width, height);
|
||
});
|
||
|
||
// Restore the context state
|
||
ctx.restore();
|
||
}
|
||
|
||
function isPointInTriangle(x, y, wall) {
|
||
if (wall.shape !== 'triangular') return true;
|
||
|
||
const baseWidth = wall.width * SCALE_FACTOR;
|
||
const height = wall.height * SCALE_FACTOR;
|
||
const topX = PADDING + (baseWidth / 2);
|
||
const topY = PADDING;
|
||
const bottomLeftX = PADDING;
|
||
const bottomLeftY = PADDING + height;
|
||
const bottomRightX = PADDING + baseWidth;
|
||
const bottomRightY = PADDING + height;
|
||
|
||
// Calculate barycentric coordinates
|
||
const denominator = (bottomLeftY - bottomRightY) * (topX - bottomRightX) +
|
||
(bottomRightX - bottomLeftX) * (topY - bottomRightY);
|
||
|
||
const a = ((bottomLeftY - bottomRightY) * (x - bottomRightX) +
|
||
(bottomRightX - bottomLeftX) * (y - bottomRightY)) / denominator;
|
||
const b = ((bottomRightY - topY) * (x - bottomRightX) +
|
||
(topX - bottomRightX) * (y - bottomRightY)) / denominator;
|
||
const c = 1 - a - b;
|
||
|
||
return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1;
|
||
}
|
||
|
||
function drawBoards(ctx, wallData) {
|
||
wallData.boardRects = [];
|
||
let cutBoardsInfo = []; // Store info about cut boards for later
|
||
|
||
// For triangular walls, create clipping path once
|
||
if (wallData.wall.shape === 'triangular') {
|
||
ctx.save();
|
||
ctx.beginPath();
|
||
const baseWidth = wallData.wall.width * SCALE_FACTOR;
|
||
const wallHeight = wallData.wall.height * SCALE_FACTOR;
|
||
ctx.moveTo(PADDING, PADDING + wallHeight);
|
||
ctx.lineTo(PADDING + baseWidth, PADDING + wallHeight);
|
||
ctx.lineTo(PADDING + (baseWidth / 2), PADDING);
|
||
ctx.closePath();
|
||
ctx.clip();
|
||
}
|
||
|
||
// Draw all boards
|
||
wallData.boards.forEach((board, index) => {
|
||
ctx.fillStyle = board.is_cut ? '#FFA726' : '#4CAF50';
|
||
ctx.strokeStyle = '#000';
|
||
ctx.lineWidth = 1;
|
||
|
||
const x = PADDING + (board.x * SCALE_FACTOR);
|
||
const y = PADDING + ((wallData.wall.height - board.y - board.height) * SCALE_FACTOR);
|
||
const width = board.width * SCALE_FACTOR;
|
||
const height = board.height * SCALE_FACTOR;
|
||
|
||
// Store board position and dimensions for hover detection
|
||
wallData.boardRects.push({
|
||
x, y, width, height,
|
||
boardData: {
|
||
width: board.width,
|
||
height: board.height,
|
||
is_cut: board.is_cut,
|
||
y_position: board.y,
|
||
x_position: board.x
|
||
}
|
||
});
|
||
|
||
// Draw board
|
||
ctx.fillRect(x, y, width, height);
|
||
ctx.strokeRect(x, y, width, height);
|
||
|
||
// Store cut board info for later drawing
|
||
if (board.is_cut) {
|
||
cutBoardsInfo.push({
|
||
x, y, width, height,
|
||
board: board,
|
||
wallData: wallData
|
||
});
|
||
}
|
||
|
||
// Draw dimensions if space allows
|
||
if (isPointInTriangle(x + width/2, y + height/2, wallData.wall)) {
|
||
ctx.fillStyle = '#000';
|
||
ctx.font = '10px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
|
||
// Format dimensions
|
||
const widthText = board.width.toFixed(2) + 'm';
|
||
const heightText = board.height.toFixed(2) + 'm';
|
||
|
||
// Draw width dimension if board is wide enough
|
||
if (width > 40) {
|
||
ctx.fillText(widthText, x + width/2, y + height/2);
|
||
}
|
||
|
||
// Draw height dimension if board is tall enough and board.x matches the calculated offset
|
||
if (height > 30 &&
|
||
(wallData.wall.shape === 'rectangular' ? board.x === 0 :
|
||
Math.abs(board.x - (board.y / wallData.wall.height) * (wallData.wall.width / 2)) < 0.01)) {
|
||
ctx.save();
|
||
ctx.translate(x - 5, y + height/2);
|
||
ctx.rotate(-Math.PI/2);
|
||
ctx.fillText(heightText, 0, 0);
|
||
ctx.restore();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Restore context if we used clipping
|
||
if (wallData.wall.shape === 'triangular') {
|
||
ctx.restore();
|
||
}
|
||
|
||
// Draw dotted lines for cut boards after clipping is restored
|
||
cutBoardsInfo.forEach(info => {
|
||
ctx.save();
|
||
ctx.strokeStyle = '#666';
|
||
ctx.setLineDash([5, 5]); // Create dotted line pattern
|
||
ctx.beginPath();
|
||
|
||
// Draw vertical dotted line at the end of the cut board
|
||
if (info.board.width < info.wallData.board.width) {
|
||
const fullWidth = info.wallData.board.width * SCALE_FACTOR;
|
||
ctx.moveTo(info.x + info.width, info.y);
|
||
ctx.lineTo(info.x + fullWidth, info.y);
|
||
ctx.moveTo(info.x + info.width, info.y + info.height);
|
||
ctx.lineTo(info.x + fullWidth, info.y + info.height);
|
||
ctx.moveTo(info.x + fullWidth, info.y);
|
||
ctx.lineTo(info.x + fullWidth, info.y + info.height);
|
||
}
|
||
|
||
// Draw horizontal dotted line at the top of cut board for height cuts
|
||
if (info.board.height < info.wallData.board.height) {
|
||
const fullHeight = info.wallData.board.height * SCALE_FACTOR;
|
||
ctx.moveTo(info.x, info.y);
|
||
ctx.lineTo(info.x, info.y - (fullHeight - info.height));
|
||
ctx.moveTo(info.x + info.width, info.y);
|
||
ctx.lineTo(info.x + info.width, info.y - (fullHeight - info.height));
|
||
ctx.moveTo(info.x, info.y - (fullHeight - info.height));
|
||
ctx.lineTo(info.x + info.width, info.y - (fullHeight - info.height));
|
||
}
|
||
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
});
|
||
}
|
||
|
||
function createWallSection(wallData, index) {
|
||
const section = document.createElement('div');
|
||
section.className = 'wall-section';
|
||
|
||
// Create wall title
|
||
const title = document.createElement('h2');
|
||
title.textContent = wallData.wall.name;
|
||
section.appendChild(title);
|
||
|
||
// Create dimensions div
|
||
const dimensions = document.createElement('div');
|
||
dimensions.className = 'dimensions';
|
||
dimensions.id = `dimensions-${index}`;
|
||
section.appendChild(dimensions);
|
||
|
||
// Create stats div
|
||
const stats = document.createElement('div');
|
||
stats.className = 'stats';
|
||
section.appendChild(stats);
|
||
|
||
// Create canvas container
|
||
const canvasContainer = document.createElement('div');
|
||
canvasContainer.className = 'canvas-container';
|
||
section.appendChild(canvasContainer);
|
||
|
||
// Create canvas element
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 800;
|
||
canvas.height = 600;
|
||
canvasContainer.appendChild(canvas);
|
||
|
||
// Add mouse event listeners for board info
|
||
const boardInfo = document.getElementById('boardInfo');
|
||
canvas.addEventListener('mousemove', (event) => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = event.clientX - rect.left;
|
||
const y = event.clientY - rect.top;
|
||
|
||
// Check if mouse is over any board
|
||
let found = false;
|
||
for (const boardRect of wallData.boardRects) {
|
||
if (x >= boardRect.x && x <= boardRect.x + boardRect.width &&
|
||
y >= boardRect.y && y <= boardRect.y + boardRect.height) {
|
||
const data = boardRect.boardData;
|
||
boardInfo.style.display = 'block';
|
||
boardInfo.style.left = (event.pageX + 10) + 'px';
|
||
boardInfo.style.top = (event.pageY + 10) + 'px';
|
||
boardInfo.innerHTML = `
|
||
Position: (${data.x_position.toFixed(2)}m, ${data.y_position.toFixed(2)}m)<br>
|
||
Size: ${data.width.toFixed(2)}m × ${data.height.toFixed(2)}m<br>
|
||
${data.is_cut ? 'Cut board' : 'Full board'}
|
||
`;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
boardInfo.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('mouseleave', () => {
|
||
boardInfo.style.display = 'none';
|
||
});
|
||
|
||
// Get the canvas context
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
// Draw wall
|
||
drawWall(ctx, wallData.wall);
|
||
|
||
// Draw boards
|
||
drawBoards(ctx, wallData);
|
||
|
||
// Draw windows (moved after boards)
|
||
drawWindows(ctx, wallData.wall);
|
||
|
||
// Add dimensions text first
|
||
let windowsInfo = '';
|
||
if (wallData.wall.windows && wallData.wall.windows.length > 0) {
|
||
windowsInfo = '<br>Windows:<br>' + wallData.wall.windows.map((w, i) =>
|
||
`Window ${i + 1}: Position (${w.left}m from left, ${w.bottom}m from bottom), Size: ${w.width}m × ${w.height}m`
|
||
).join('<br>');
|
||
}
|
||
|
||
dimensions.innerHTML = `
|
||
Wall dimensions: ${wallData.wall.width}m × ${wallData.wall.height}m<br>
|
||
Wall shape: ${wallData.wall.shape}<br>
|
||
Board dimensions: ${wallData.board.width}m × ${wallData.board.height}m
|
||
${windowsInfo}
|
||
`;
|
||
|
||
// Draw dimension lines and labels
|
||
ctx.strokeStyle = '#666';
|
||
ctx.fillStyle = '#666';
|
||
ctx.lineWidth = 1;
|
||
ctx.font = '12px Arial';
|
||
ctx.textAlign = 'center';
|
||
|
||
// Width dimension
|
||
const y = wallData.wall.height * SCALE_FACTOR + PADDING + 20;
|
||
ctx.beginPath();
|
||
ctx.moveTo(PADDING, y);
|
||
ctx.lineTo(PADDING + wallData.wall.width * SCALE_FACTOR, y);
|
||
ctx.stroke();
|
||
ctx.fillText(
|
||
`${wallData.wall.width}m`,
|
||
PADDING + (wallData.wall.width * SCALE_FACTOR) / 2,
|
||
y + 15
|
||
);
|
||
|
||
// Height dimension
|
||
const x = PADDING - 20;
|
||
ctx.textAlign = 'right';
|
||
ctx.save();
|
||
ctx.translate(x, PADDING + (wallData.wall.height * SCALE_FACTOR) / 2);
|
||
ctx.rotate(-Math.PI / 2);
|
||
ctx.fillText(`${wallData.wall.height}m`, 0, 0);
|
||
ctx.restore();
|
||
|
||
// Add stats to the stats div
|
||
stats.innerHTML = `
|
||
Total boards: ${wallData.boards.length}<br>
|
||
Full boards: ${wallData.boards.filter(b => !b.is_cut).length}<br>
|
||
Cut boards: ${wallData.boards.filter(b => b.is_cut).length}
|
||
`;
|
||
|
||
return section;
|
||
}
|
||
|
||
function renderWallSections() {
|
||
console.log('Starting renderWallSections...');
|
||
const wallSections = document.getElementById('wallSections');
|
||
if (!wallSections) {
|
||
console.error('Could not find wallSections element!');
|
||
return;
|
||
}
|
||
wallSections.innerHTML = '';
|
||
|
||
// Calculate totals across all walls
|
||
console.log('Calculating totals...');
|
||
const totalBoards = wallsData.reduce((sum, wall) => sum + wall.boards.length, 0);
|
||
const totalFullBoards = wallsData.reduce((sum, wall) => sum + wall.boards.filter(b => !b.is_cut).length, 0);
|
||
const totalCutBoards = wallsData.reduce((sum, wall) => sum + wall.boards.filter(b => b.is_cut).length, 0);
|
||
console.log('Totals calculated:', { totalBoards, totalFullBoards, totalCutBoards });
|
||
|
||
// Render each wall section
|
||
console.log('Rendering wall sections...');
|
||
wallsData.forEach((wallData, index) => {
|
||
console.log(`Creating wall section ${index}:`, wallData.wall.name);
|
||
const section = createWallSection(wallData, index);
|
||
wallSections.appendChild(section);
|
||
});
|
||
|
||
// Update total summary
|
||
console.log('Updating total summary...');
|
||
const totalSummary = document.getElementById('totalSummary');
|
||
if (!totalSummary) {
|
||
console.error('Could not find totalSummary element!');
|
||
return;
|
||
}
|
||
totalSummary.innerHTML = `
|
||
<h2>Total Summary</h2>
|
||
Total boards across all walls: ${totalBoards}<br>
|
||
Full boards: ${totalFullBoards}<br>
|
||
Cut boards: ${totalCutBoards}
|
||
`;
|
||
console.log('Rendering complete.');
|
||
}
|
||
|
||
// Only render if we have data
|
||
if (typeof wallsData !== 'undefined') {
|
||
console.log('Starting render with data...');
|
||
renderWallSections();
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |