524 lines
16 KiB
JavaScript
524 lines
16 KiB
JavaScript
// 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 {
|
||
window.open(`${API_BASE}/download/${currentMazeId}?solution=false`, '_blank');
|
||
showSuccess('Downloading maze image...');
|
||
} catch (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;
|
||
}
|
||
}
|