diff --git a/script.js b/script.js index 1fee6d1..e2ca09d 100644 --- a/script.js +++ b/script.js @@ -7,7 +7,7 @@ const properties = { // Each cell will have these properties by default color: "#ccc", pheromone: 1.0 } -window.startingPoint = 'red'; +window.startingPoint = 'red'; // Global color for the starting point window.grid = []; for (let x = 0; x < window.gridWidth; x++) { let cols = []; @@ -18,10 +18,11 @@ let start; document.getElementById("widget_status").textContent = "Detenida"; +// Load last used theme if available if (localStorage.getItem('theme')) { localStorage.getItem('theme') == 'dark' ? applyTheme(1) : applyTheme(); } else { applyTheme(darkThemeMq.matches); } window.onload = function () { - drawElements(scenarios[getSelectedScenario()]) + drawElements(scenarios[getSelectedScenario()]) // Initial drawing darkThemeMq.addEventListener("change", e => { applyTheme(e.matches); localStorage.setItem('theme', e.matches ? 'dark' : 'light'); @@ -29,31 +30,36 @@ window.onload = function () { canvas.addEventListener("click", function (event) { const rect = canvas.getBoundingClientRect(); + let clickedOnFloor = true; start = { x: Math.floor((event.clientX - rect.left) / window.cellSize), y: Math.floor((event.clientY - rect.top) / window.cellSize) }; document.getElementById("widget_status").textContent = "Detenida"; - let state = true; for (let i = start.x - 1; i <= start.x + 1; i++) { for (let j = start.y - 1; j <= start.y + 1; j++) { if (window.grid[i][j].color != "#ccc") { - state = false; + clickedOnFloor = false; break; } } } - if (state) { + // If clicked on a valid position + if (clickedOnFloor) { console.log(`Clicked at (${start.x}, ${start.y})`); drawElements(scenarios[getSelectedScenario()]); setColor([start.x - 1, 2], [start.y - 1, 2], window.startingPoint); } else { + + // If clicked on the current starting point if (window.grid[start.x][start.y].color === window.startingPoint) { window.showToast("Por favor, seleccione otro punto o inicie la simulación.") - } else { window.showToast("No puedes empezar ahí. Haz clic en el suelo de la habitación."); } + } else { window.showToast("No puedes empezar ahí. Haz clic en el suelo de la habitación."); } // If clicked elsewhere } }); document.getElementById("start").addEventListener("click", function () { + + // If there is any point set and the simulation is down if (start && document.getElementById("widget_status").textContent == "Detenida") { runSimulations(start, Number(document.getElementById("alpha").value), @@ -74,6 +80,8 @@ window.onload = function () { const containerRect = canvasContainer.getBoundingClientRect(); const selected = getSelectedScenario(); [window.gridWidth, window.gridHeight] = [dimensions[selected].gridWidth, dimensions[selected].gridHeight]; + + // Set unitary scaling factor (USF) window.cellSize = Math.min( Math.floor(containerRect.width / window.gridWidth), Math.floor(containerRect.height / window.gridHeight) diff --git a/src/ant.js b/src/ant.js index d44c6b6..f5e4239 100644 --- a/src/ant.js +++ b/src/ant.js @@ -39,10 +39,11 @@ export default class Ant { for (let i = 0; i < directions.length; i++) { const euclideanDistance = moduleToGoal(this.x + directions[i].x, this.y + directions[i].y); const invertedDistance = (Math.abs(directions[i].x) + Math.abs(directions[i].y)) === 2 ? 1 / Math.sqrt(2) : 1; - let weigh = Math.pow(pheromone[i], this.alpha) * Math.pow(invertedDistance, this.beta); // Generic cost calculation formula for Ant Colony Optimization systems + let weigh = Math.pow(pheromone[i], this.alpha) * Math.pow(invertedDistance, this.beta); // Generic weigh calculation formula for Ant Colony Optimization systems weighs.push(weigh); + // Update best distance if (euclideanDistance < distance) { distance = euclideanDistance; bestDirectionIndex = i; @@ -51,6 +52,7 @@ export default class Ant { weighs[bestDirectionIndex] += this.correctionRatio; // Algorithm improvement aside the original ACO algorithm + // Specific weigh optimization for the algorithm const lastVisitedCellIndex = this.directions.findIndex(direction => this.visited[this.visited.length - 1].x === this.x - direction.x && this.visited[this.visited.length - 1].y === this.y - direction.y @@ -58,7 +60,7 @@ export default class Ant { if (lastVisitedCellIndex > 0) weighs[lastVisitedCellIndex - 1] /= 3; if (lastVisitedCellIndex < 0) weighs[lastVisitedCellIndex + 1] /= 3; - + // Roulette-wheelish selection const probabilities = calcProbabilities(weighs); const rand = Math.random(); let cumulative = 0; @@ -70,6 +72,8 @@ export default class Ant { } } getDirs(x, y, avoid) { + + // Just ensures to take x and y values from somewhere if (!(x && y)) { x = this.x; y = this.y; @@ -77,42 +81,49 @@ export default class Ant { return this.directions.filter(direction => { const newX = x + direction.x; const newY = y + direction.y; - const isObject = this.obstacle.some(object => newX === object.x && newY === object.y); - const isVisited = this.visited.some(visit => newX === visit.x && newY === visit.y); - const isAvoided = avoid && avoid.some(deadEnd => deadEnd.x === newX && deadEnd.y === newY); + const isObject = this.obstacle.some(object => newX === object.x && newY === object.y); // Whether is an object + const isVisited = this.visited.some(visit => newX === visit.x && newY === visit.y); // Whether is has been visited before + const isAvoided = avoid && avoid.some(deadEnd => deadEnd.x === newX && deadEnd.y === newY); // Whether is it marked as a dead end return !isObject && !isVisited && !isAvoided; }); } move(grid, directions, goal) { + + // Get the pheromone from every available direction const total_pheromone = directions.map(direction => { const inX = this.x + direction.x; const inY = this.y + direction.y; return grid[inX][inY].pheromone; }); - const index = this._calcCost(total_pheromone, directions, goal); + const index = this._calcCost(total_pheromone, directions, goal); // This gets which option is the best for the working conditions + + // Update new position const newX = this.x + directions[index].x; const newY = this.y + directions[index].y; - grid[newX][newY].pheromone += this.deposit; + grid[newX][newY].pheromone += this.deposit; // Add pheromone to the moved cell // Update ant's position and visited path this.x = newX; this.y = newY; - const distance = (Math.abs(directions[index].x) + Math.abs(directions[index].y)) === 2 ? Math.sqrt(2) : 1; + const distance = (Math.abs(directions[index].x) + Math.abs(directions[index].y)) === 2 ? Math.sqrt(2) : 1; // If the direction moves in both x and y axis, then the distance is root of two. If not, the distance is one return [{ x: newX, y: newY }, distance]; } revertMove() { - const deadEnd = this.visited.pop(); // Remove from visited the last element + const deadEnd = this.visited.pop(); // Remove the last element from the ant's trace const { x, y } = this.visited[this.visited.length - 1]; // Get the new last element return { x: x, y: y, avoid: deadEnd }; } checkExit(grid, state) { - const color = state ? "#02b200" : "red"; + const color = state ? "#02b200" : "red"; // Tells to look for the exit or for the starting point let isExit = false; + + // If any of the next cells looks like an exit, returns true and finishes the loop for (const direction of this.directions) { const exitX = this.x + direction.x; const exitY = this.y + direction.y; + if (grid[exitX][exitY].color === color) { isExit = true; break; diff --git a/src/layouts.js b/src/layouts.js index 1e9009a..2b23f0b 100644 --- a/src/layouts.js +++ b/src/layouts.js @@ -225,6 +225,8 @@ export function getSelectedScenario() { return document.querySelector('input[nam document.getElementById('scenarios-form').addEventListener('change', () => { const selected = getSelectedScenario(); [window.gridWidth, window.gridHeight] = [dimensions[selected].gridWidth, dimensions[selected].gridHeight]; + + // Set unitary scaling factor (USF) window.cellSize = Math.min( Math.floor(containerRect.width / window.gridWidth), Math.floor(containerRect.height / window.gridHeight) diff --git a/src/source.js b/src/source.js index 950db89..1e00b8a 100644 --- a/src/source.js +++ b/src/source.js @@ -5,6 +5,7 @@ const paint = canvas.getContext("2d"); const [startingAnt, returningAnt] = ["green", "darkgreen"]; let stepNumber = 0; +// This function returns the coordinates of every obstacle in the scenario function getObjects(object) { let objects = []; if (Array.isArray(object)) { @@ -22,6 +23,8 @@ function getObjects(object) { } return objects; } + +// 'Main' function async function antStart(state, start, initial, alpha, beta, rho, deposit, objects) { return new Promise((resolve, reject) => { const room = scenarios[getSelectedScenario()]; @@ -30,21 +33,24 @@ async function antStart(state, start, initial, alpha, beta, rho, deposit, object let { x, y } = initial; let visited = [{ x, y }]; let deadEnds = []; - let ant = new Ant(x, y, visited, objects, state ? Math.pow(alpha, 2) : alpha, beta, deposit, window.gridWidth, window.gridHeight); + let ant = new Ant( + x, y, // Initial coordinates of the ant + visited, objects, // Different types of elements in the scenario + alpha, beta, deposit, // User-provided heuristic parameters + window.gridWidth, window.gridHeight // Scenario dimensions + ); function moveAnt() { - function getNearestPoint(x0, y0, objects) { + function getNearestExit(x0, y0, objects) { let nearest = {}; let currentDistance = Infinity; for (const object of objects) { - const distance = Math.sqrt(Math.pow(object.x - x0, 2) + Math.pow(object.y - y0, 2)); + const distance = Math.sqrt(Math.pow(object.x - x0, 2) + Math.pow(object.y - y0, 2)); // Euclidean formula if (distance < currentDistance) { nearest = { x: object.x, y: object.y }; currentDistance = distance; - } else { - break; - } + } else { break; } } return nearest; } @@ -58,7 +64,7 @@ async function antStart(state, start, initial, alpha, beta, rho, deposit, object deadEnds.push(revert.avoid); [x, y] = [revert.x, revert.y]; newdirs = ant.getDirs(x, y, deadEnds); - } while (newdirs.length < 1); + } while (newdirs.length < 1); // Until the ant has an alternative path to follow ant.x = x; ant.y = y; dirs = newdirs; @@ -68,16 +74,13 @@ async function antStart(state, start, initial, alpha, beta, rho, deposit, object const exit = room.exits[key]; exits.push(exit.color); } - let nearestPoint = getNearestPoint(x, y, getObjects(state ? exits : window.startingPoint)); - const [movedTo, distance] = ant.move(window.grid, dirs, nearestPoint); + let nearestExit = getNearestExit(x, y, getObjects(state ? exits : window.startingPoint)); + const [movedTo, distance] = ant.move(window.grid, dirs, nearestExit); moveCount++; visited.push({ x: movedTo.x, y: movedTo.y }); // Evaporate pheromone - for (const visit of visited) { - window.grid[visit.x][visit.y].pheromone *= (1 - rho); - if (window.grid[visit.x][visit.y].pheromone < 0.00001) window.grid[visit.x][visit.y].pheromone = 0; - } + for (const visit of visited) { window.grid[visit.x][visit.y].pheromone *= (1 - rho); } [x, y] = [movedTo.x, movedTo.y]; ant.x = movedTo.x; @@ -91,7 +94,7 @@ async function antStart(state, start, initial, alpha, beta, rho, deposit, object for (const visit of visited) setColor(visit.x, visit.y, state ? startingAnt : returningAnt); setColor([start.x - 1, 2], [start.y - 1, 2], window.startingPoint); - // Speed regulation + // Dynamic speed regulation if (moveCount % Number(document.getElementById("ant_speed").value) === 0 && Number(document.getElementById("ant_speed").value) != Number(document.getElementById("ant_speed").max)) requestAnimationFrame(moveAnt); else moveAnt(); // Continue the loop @@ -103,6 +106,7 @@ async function antStart(state, start, initial, alpha, beta, rho, deposit, object requestAnimationFrame(moveAnt); // Start the loop }); } + export async function applyTheme(isDark) { if (isDark) { document.body.classList.add('dark-mode'); @@ -118,6 +122,8 @@ export async function applyTheme(isDark) { window.applyTheme = applyTheme; } export async function setColor(x, y, color) { + + // If the parameters are intervals, the function operates with the sum of the first and the second const X = Array.isArray(x) ? x[0] : x; const endX = Array.isArray(x) ? x[0] + x[1] : x; const Y = Array.isArray(y) ? y[0] : y; @@ -125,9 +131,9 @@ export async function setColor(x, y, color) { for (let i = X; i <= endX; i++) { for (let j = Y; j <= endY; j++) { try { - window.grid[i][j].color = color; + window.grid[i][j].color = color; // Make low-level changes paint.fillStyle = window.grid[i][j].color; - paint.fillRect(i * window.cellSize, j * window.cellSize, window.cellSize, window.cellSize); + paint.fillRect(i * window.cellSize, j * window.cellSize, window.cellSize, window.cellSize); // Make graphical changes } catch (error) { return; } @@ -136,6 +142,8 @@ export async function setColor(x, y, color) { } export async function drawElements(room) { const { floor, walls, windows, exits, elements } = room; + + // Iterates one by one and displays the objects respectingly for (const item in room) { switch (item) { case 'floor': @@ -210,7 +218,7 @@ export async function runSimulations(start, alpha, beta, rho, deposit, steps) { let bestPath = []; document.getElementById("widget_status").textContent = "En ejecucción"; - // Reset trace through simulations + // Reset pheromone trace through simulations for (let i = 0; i < window.gridWidth; i++) { for (let j = 0; j < window.gridHeight; j++) { if (window.grid[i][j].pheromone != 1.0) window.grid[i][j].pheromone = 1.0; @@ -222,6 +230,7 @@ export async function runSimulations(start, alpha, beta, rho, deposit, steps) { let stringPath = ''; let objects = []; + // Get the color of every wall, window and all the elements for (const key in room) { const element = room[key]; if (key === 'walls' || key === 'windows' || key === 'elements') { @@ -237,11 +246,13 @@ export async function runSimulations(start, alpha, beta, rho, deposit, steps) { } } drawElements(scenarios[getSelectedScenario()]); - for (const path of bestPath) { setColor(path.x, path.y, startingAnt) }; + for (const path of bestPath) setColor(path.x, path.y, startingAnt); document.getElementById("widget_step").textContent = `${stepNumber + 1} de ${steps}`; elements = getObjects(objects); [currentPoint, newDistance, visited] = await antStart(1, start, currentPoint, alpha, beta, rho, deposit, elements); + + // Check if the new distance is lower than the older one stored or stores it if there was no stored distance before if (newDistance < oldDistance || oldDistance == 0) { oldDistance = newDistance; bestPath = visited; @@ -249,13 +260,14 @@ export async function runSimulations(start, alpha, beta, rho, deposit, steps) { for (const path of bestPath) setColor(path.x, path.y, startingAnt); visited = []; elements = []; - for (const key in room.exits) { - const exit = room.exits[key]; - objects.push(exit.color); // Added all exits as objects for the ant to avoid - } + + // Include all exits as objects for the ant to avoid and the returning + for (const key in room.exits) objects.push(room.exits[key].color); elements = getObjects(objects); [currentPoint, newDistance, visited] = await antStart(0, start, currentPoint, alpha, beta, rho, deposit, elements); + + // Check if the new distance is lower than the older one stored if (newDistance < oldDistance) { oldDistance = newDistance; bestPath = visited; @@ -283,7 +295,9 @@ export async function runSimulations(start, alpha, beta, rho, deposit, steps) { for (const path of bestPath) setColor(path.x, path.y, window.startingPoint); } export function roundValues(obj) { - if (typeof obj === 'object' && obj !== null) { + + // Iterates on each value to check if it is a number + if (typeof obj === 'object' && obj !== null) { // If it is not a number, the value remains the same if (Array.isArray(obj)) { return obj.map(item => roundValues(item)); } else { @@ -295,16 +309,16 @@ export function roundValues(obj) { } return roundedObj; } - } else if (typeof obj === 'number') { + } else if (typeof obj === 'number') { // Rounds the value if it is a number return Math.round(obj); } + + // Returns the number-rounded object return obj; } window.showToast = async function (t) { const toast = document.getElementById('toast'); toast.textContent = t; toast.classList.add('show'); - setTimeout(() => { - toast.classList.remove('show'); - }, 4000); + setTimeout(() => { toast.classList.remove('show'); }, 4000); // Shows toast for 4 secs }; \ No newline at end of file