Files
python-maze/web/static/js/app.js
2025-11-20 23:38:31 -05:00

554 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Main application logic
const API_BASE = '/api';
let currentMazeId = null;
let currentMazeData = null;
let animationState = {
isAnimating: false,
isPaused: false,
currentStep: 0,
visitedCells: [],
solutionPath: [],
animationId: null,
speed: 50 // milliseconds per step
};
// DOM Elements
const generateBtn = document.getElementById('generateBtn');
const visualizeBtn = document.getElementById('visualizeBtn');
const downloadBtn = document.getElementById('downloadBtn');
const saveBtn = document.getElementById('saveBtn');
const loadBtn = document.getElementById('loadBtn');
const solveDfsBtn = document.getElementById('solveDfsBtn');
const solveBfsBtn = document.getElementById('solveBfsBtn');
const analyzeBtn = document.getElementById('analyzeBtn');
const benchmarkBtn = document.getElementById('benchmarkBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const speedSlider = document.getElementById('speedSlider');
const algorithmSelect = document.getElementById('algorithm');
const rowsInput = document.getElementById('rows');
const colsInput = document.getElementById('cols');
const seedInput = document.getElementById('seed');
const resultsContent = document.getElementById('resultsContent');
const mazeInfo = document.getElementById('mazeInfo');
const animationControls = document.getElementById('animationControls');
// Disable buttons initially
function disableActionButtons() {
visualizeBtn.disabled = true;
downloadBtn.disabled = true;
saveBtn.disabled = true;
solveDfsBtn.disabled = true;
solveBfsBtn.disabled = true;
analyzeBtn.disabled = true;
}
function enableActionButtons() {
visualizeBtn.disabled = false;
downloadBtn.disabled = false;
saveBtn.disabled = false;
solveDfsBtn.disabled = false;
solveBfsBtn.disabled = false;
analyzeBtn.disabled = false;
}
disableActionButtons();
// Event Listeners
generateBtn.addEventListener('click', generateMaze);
visualizeBtn.addEventListener('click', visualizeMaze);
downloadBtn.addEventListener('click', downloadMaze);
saveBtn.addEventListener('click', saveMaze);
loadBtn.addEventListener('click', showLoadModal);
solveDfsBtn.addEventListener('click', () => solveMaze('dfs'));
solveBfsBtn.addEventListener('click', () => solveMaze('bfs'));
analyzeBtn.addEventListener('click', analyzeMaze);
benchmarkBtn.addEventListener('click', runBenchmark);
pauseBtn.addEventListener('click', togglePause);
stopBtn.addEventListener('click', stopAnimation);
speedSlider.addEventListener('input', updateSpeed);
// Generate Maze
async function generateMaze() {
const algorithm = algorithmSelect.value;
const rows = parseInt(rowsInput.value);
const cols = parseInt(colsInput.value);
const seed = seedInput.value ? parseInt(seedInput.value) : null;
if (rows < 5 || rows > 50 || cols < 5 || cols > 50) {
showError('Dimensions must be between 5 and 50');
return;
}
setLoading(generateBtn, true);
try {
const response = await fetch(`${API_BASE}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ algorithm, rows, cols, seed })
});
const data = await response.json();
if (data.success) {
currentMazeId = data.id;
currentMazeData = data.maze;
enableActionButtons();
showSuccess('Maze generated successfully!');
displayMazeInfo(data.maze);
visualizeMaze();
} else {
showError(data.error || 'Failed to generate maze');
}
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(generateBtn, false);
}
}
// Visualize Maze
function visualizeMaze() {
if (!currentMazeData) {
showError('No maze to visualize');
return;
}
renderMaze(currentMazeData);
showInfo('Maze visualized');
}
// Download Maze
async function downloadMaze() {
if (currentMazeId === null) {
showError('No maze to download');
return;
}
try {
// Create a temporary link and trigger download
const downloadUrl = `${API_BASE}/download/${currentMazeId}?solution=false&format=png`;
const response = await fetch(downloadUrl);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Download failed' }));
throw new Error(errorData.error || `Server returned ${response.status}`);
}
const blob = await response.blob();
// Verify we got an image
if (!blob.type.startsWith('image/')) {
throw new Error('Invalid response: not an image');
}
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `maze_${currentMazeId}_${currentMazeData.algorithm.replace(/[^a-z0-9]/gi, '_')}.png`;
document.body.appendChild(a);
a.click();
// Clean up after a short delay
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
showSuccess('Maze image downloaded!');
} catch (error) {
console.error('Download error:', error);
showError('Failed to download: ' + error.message);
}
}
// Save Maze
async function saveMaze() {
if (currentMazeId === null) {
showError('No maze to save');
return;
}
const filename = prompt('Enter filename:', `maze_${currentMazeId}`);
if (!filename) return;
try {
const response = await fetch(`${API_BASE}/save/${currentMazeId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename })
});
const data = await response.json();
if (data.success) {
showSuccess(`Maze saved to: ${data.filepath}`);
} else {
showError(data.error || 'Failed to save maze');
}
} catch (error) {
showError('Network error: ' + error.message);
}
}
// Load Maze Modal
async function showLoadModal() {
const modal = document.getElementById('loadModal');
const fileList = document.getElementById('fileList');
const closeBtn = document.getElementById('closeModal');
try {
const response = await fetch(`${API_BASE}/saved-mazes`);
const data = await response.json();
if (data.files && data.files.length > 0) {
fileList.innerHTML = data.files.map(file =>
`<div class="file-item" onclick="loadMazeFile('${file}')">${file}</div>`
).join('');
} else {
fileList.innerHTML = '<p class="placeholder-text">No saved mazes found</p>';
}
modal.classList.add('active');
closeBtn.onclick = () => modal.classList.remove('active');
modal.onclick = (e) => {
if (e.target === modal) modal.classList.remove('active');
};
} catch (error) {
showError('Failed to load file list: ' + error.message);
}
}
async function loadMazeFile(filename) {
const modal = document.getElementById('loadModal');
modal.classList.remove('active');
try {
const response = await fetch(`${API_BASE}/load`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename })
});
const data = await response.json();
if (data.success) {
currentMazeId = data.id;
currentMazeData = data.maze;
enableActionButtons();
showSuccess(`Loaded: ${filename}`);
displayMazeInfo(data.maze);
visualizeMaze();
} else {
showError(data.error || 'Failed to load maze');
}
} catch (error) {
showError('Network error: ' + error.message);
}
}
// Solve Maze with Animation
async function solveMaze(algorithm) {
if (currentMazeId === null) {
showError('No maze to solve');
return;
}
// Stop any ongoing animation
stopAnimation();
const btn = algorithm === 'dfs' ? solveDfsBtn : solveBfsBtn;
setLoading(btn, true);
try {
const response = await fetch(`${API_BASE}/solve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ maze_id: currentMazeId, algorithm })
});
const data = await response.json();
setLoading(btn, false);
if (data.success) {
// Start animated solving
startSolvingAnimation(data);
displaySolutionInfo(data);
} else {
showError('Failed to solve maze');
}
} catch (error) {
showError('Network error: ' + error.message);
setLoading(btn, false);
}
}
// Animation Control Functions
function startSolvingAnimation(solutionData) {
animationState.isAnimating = true;
animationState.isPaused = false;
animationState.currentStep = 0;
animationState.visitedCells = solutionData.visited;
animationState.solutionPath = solutionData.path;
// Show animation controls
animationControls.style.display = 'block';
pauseBtn.textContent = 'PAUSE';
// Disable solve buttons during animation
solveDfsBtn.disabled = true;
solveBfsBtn.disabled = true;
// Start animation
animateStep();
}
function animateStep() {
if (!animationState.isAnimating || animationState.isPaused) {
return;
}
const { currentStep, visitedCells, solutionPath } = animationState;
if (currentStep < visitedCells.length) {
// Show visited cells up to current step
const visibleVisited = visitedCells.slice(0, currentStep + 1);
// Don't show solution path yet, only when all cells are visited
renderMaze(currentMazeData, visibleVisited, null);
animationState.currentStep++;
// Schedule next step
animationState.animationId = setTimeout(animateStep, animationState.speed);
} else if (currentStep < visitedCells.length + solutionPath.length) {
// Now animate the solution path
const pathStep = currentStep - visitedCells.length;
const visiblePath = solutionPath.slice(0, pathStep + 1);
renderMaze(currentMazeData, visitedCells, visiblePath);
animationState.currentStep++;
// Schedule next step (slower for solution path)
animationState.animationId = setTimeout(animateStep, animationState.speed * 2);
} else {
// Animation complete
finishAnimation();
}
}
function togglePause() {
if (!animationState.isAnimating) return;
animationState.isPaused = !animationState.isPaused;
if (animationState.isPaused) {
pauseBtn.textContent = 'RESUME';
if (animationState.animationId) {
clearTimeout(animationState.animationId);
}
} else {
pauseBtn.textContent = 'PAUSE';
animateStep();
}
}
function stopAnimation() {
if (!animationState.isAnimating) return;
animationState.isAnimating = false;
animationState.isPaused = false;
if (animationState.animationId) {
clearTimeout(animationState.animationId);
animationState.animationId = null;
}
// Hide animation controls
animationControls.style.display = 'none';
// Re-enable solve buttons
solveDfsBtn.disabled = false;
solveBfsBtn.disabled = false;
// Show final result if we have solution data
if (animationState.visitedCells.length > 0 && animationState.solutionPath.length > 0) {
renderMaze(currentMazeData, animationState.visitedCells, animationState.solutionPath);
} else {
renderMaze(currentMazeData);
}
}
function finishAnimation() {
animationState.isAnimating = false;
// Hide animation controls
animationControls.style.display = 'none';
// Re-enable solve buttons
solveDfsBtn.disabled = false;
solveBfsBtn.disabled = false;
// Show final result
renderMaze(currentMazeData, animationState.visitedCells, animationState.solutionPath);
showSuccess('Solving animation complete!');
}
function updateSpeed(event) {
// Speed slider: 1 (slow) to 100 (fast)
// Convert to delay: 200ms (slow) to 10ms (fast)
const sliderValue = parseInt(event.target.value);
animationState.speed = 210 - (sliderValue * 2);
}
// Analyze Maze
async function analyzeMaze() {
if (currentMazeId === null) {
showError('No maze to analyze');
return;
}
setLoading(analyzeBtn, true);
try {
const response = await fetch(`${API_BASE}/analyze/${currentMazeId}`);
const data = await response.json();
displayAnalysisResults(data);
showSuccess('Analysis complete!');
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(analyzeBtn, false);
}
}
// Run Benchmark
async function runBenchmark() {
setLoading(benchmarkBtn, true);
showInfo('Running benchmarks... This may take a moment.');
try {
const response = await fetch(`${API_BASE}/benchmark`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'quick' })
});
const data = await response.json();
displayBenchmarkResults(data);
showSuccess('Benchmark complete!');
} catch (error) {
showError('Network error: ' + error.message);
} finally {
setLoading(benchmarkBtn, false);
}
}
// Display Functions
function displayMazeInfo(maze) {
mazeInfo.innerHTML = `
<strong>ALGORITHM:</strong> ${maze.algorithm}<br>
<strong>SIZE:</strong> ${maze.rows} × ${maze.cols}<br>
<strong>GENERATION TIME:</strong> ${maze.generationTime.toFixed(2)} ms
`;
}
function displaySolutionInfo(solution) {
const section = document.createElement('div');
section.className = 'result-section';
section.innerHTML = `
<h3>SOLUTION: ${solution.algorithm}</h3>
<div class="result-item"><strong>Path Length:</strong> ${solution.path_length} cells</div>
<div class="result-item"><strong>Cells Visited:</strong> ${solution.visited.length}</div>
<div class="result-item"><strong>Solve Time:</strong> ${solution.time_ms.toFixed(2)} ms</div>
`;
resultsContent.innerHTML = '';
resultsContent.appendChild(section);
}
function displayAnalysisResults(analysis) {
const section = document.createElement('div');
section.className = 'result-section';
section.innerHTML = `
<h3>MAZE ANALYSIS</h3>
<div class="result-item"><strong>Dimensions:</strong> ${analysis.dimensions}</div>
<div class="result-item"><strong>Total Cells:</strong> ${analysis.total_cells}</div>
<div class="result-item"><strong>Algorithm:</strong> ${analysis.algorithm}</div>
<div class="result-item"><strong>Dead Ends:</strong> ${analysis.dead_ends} (${analysis.dead_end_percentage.toFixed(1)}%)</div>
<div class="result-item"><strong>Longest Path:</strong> ${analysis.longest_path_length} cells</div>
<div class="result-item"><strong>Avg Branching:</strong> ${analysis.average_branching_factor.toFixed(2)}</div>
`;
resultsContent.innerHTML = '';
resultsContent.appendChild(section);
}
function displayBenchmarkResults(data) {
let html = '<div class="result-section"><h3>GENERATOR BENCHMARK</h3>';
html += '<table class="result-table"><tr><th>Algorithm</th><th>Size</th><th>Avg Time (ms)</th></tr>';
data.generators.results.forEach(r => {
html += `<tr><td>${r.algorithm}</td><td>${r.size}</td><td>${r.avg_time_ms}</td></tr>`;
});
html += '</table></div>';
html += '<div class="result-section"><h3>SOLVER BENCHMARK</h3>';
html += '<table class="result-table"><tr><th>Algorithm</th><th>Size</th><th>Avg Time (ms)</th><th>Path Length</th></tr>';
data.solvers.results.forEach(r => {
html += `<tr><td>${r.algorithm}</td><td>${r.size}</td><td>${r.avg_time_ms}</td><td>${r.avg_path_length}</td></tr>`;
});
html += '</table></div>';
resultsContent.innerHTML = html;
}
// Utility Functions
function showSuccess(message) {
showStatus(message, 'success');
}
function showError(message) {
showStatus(message, 'error');
}
function showInfo(message) {
showStatus(message, 'info');
}
function showStatus(message, type) {
const existing = document.querySelector('.status-message');
if (existing) existing.remove();
const div = document.createElement('div');
div.className = `status-message status-${type}`;
div.textContent = message;
resultsContent.insertBefore(div, resultsContent.firstChild);
setTimeout(() => div.remove(), 5000);
}
function setLoading(button, isLoading) {
if (isLoading) {
button.dataset.originalText = button.textContent;
button.textContent = 'LOADING...';
button.disabled = true;
} else {
button.textContent = button.dataset.originalText || button.textContent;
button.disabled = false;
}
}