Cluster flooding
Generate a grid of random labels and watch the algorithm flood each cluster of orthogonally connected cells.
The commented source
// Setup some constants
export const PAUSED = 'paused';
export const PLAYING = 'playing';
export const DONE = 'done';
const UP = 'up';
const DOWN = 'down';
const LEFT = 'left';
const RIGHT = 'right';
/**
* Return a random between 0 and max
* @param max
* @returns {number}
*/
function getRandom(max) {
return Math.floor(Math.random() * max);
}
/**
* Create the algorithm around a state object, which also drives the visualisation.
* @param app The state: settings (width, height, labels, wait, searchMode) and all counters
* @returns The controls: generate, play, pause and step
*/
export function createFlooding(app) {
let global_queue = [];
let local_queue = [];
let timer = null;
/**
* Fill the matrix with random labels and reset everything else.
*/
function generate() {
clearTimeout(timer);
global_queue = [];
local_queue = [];
app.matrix = Array.from({ length: app.height }, (_, row) =>
Array.from({ length: app.width }, (_, col) => ({
label: getRandom(app.labels),
cluster: -1,
global_queue: false,
local_queue: false,
row,
col,
})),
);
app.status = PAUSED;
app.clusters = 0;
app.steps = 0;
app.lookups = 0;
app.comparisons = 0;
app.global_queue_size = 0;
app.local_queue_size = 0;
app.max_global_queue_size = 0;
app.max_local_queue_size = 0;
app.current_global = null;
app.current_local = null;
}
/**
* Keep taking steps until paused or done.
*/
function play() {
if (app.status !== PAUSED) return;
app.status = PLAYING;
tick();
}
function tick() {
step();
if (app.status === PLAYING) {
timer = setTimeout(tick, app.wait);
}
}
function pause() {
clearTimeout(timer);
if (app.status === PLAYING) app.status = PAUSED;
}
/**
* Take a single step: the local queue goes first, so a cluster is completely flooded
* before the global queue gets to start the next one.
*/
function step() {
if (app.status === DONE) return;
// Select a random starting point
if (app.steps === 0) {
const start = app.matrix[getRandom(app.matrix.length)][getRandom(app.matrix[0].length)];
start.global_queue = true;
global_queue.push(start);
}
// Every neighbour of a clustered cell is either clustered or queued, so with
// empty queues there is nothing left to discover.
if (global_queue.length === 0 && local_queue.length === 0) {
clearTimeout(timer);
app.status = DONE;
app.current_global = null;
app.current_local = null;
return;
}
if (local_queue.length > 0) {
evaluateLocalCell();
} else {
evaluateGlobalCell();
}
app.steps = app.steps + 1;
updateQueueSizes();
}
/**
* The actual body of the algorithm.
* Evaluates the next global cell, and labels it if needed, then discovers the adjacent cells.
*/
function evaluateGlobalCell() {
const cell = app.searchMode === 'breadth' ? global_queue.shift() : global_queue.pop();
app.current_global = cell;
app.current_local = null;
cell.global_queue = false;
if (!isClustered(cell)) {
// Oh it has no label!
app.clusters = app.clusters + 1; // No label! So we found a new cluster
cell.cluster = app.clusters; // Label this for the current cluster
discoverNeighbours(cell); // Add the adjacent fields to the corresponding queue
}
}
/**
* Evaluate the next local cell to see if more flooding is needed.
*/
function evaluateLocalCell() {
const cell = local_queue.pop();
app.current_local = cell;
cell.local_queue = false;
cell.cluster = app.clusters; // Label this for the current cluster
discoverNeighbours(cell); // Add the adjacent fields to the corresponding queue, again
}
/**
* Verify if the field is labeled (-1)
* @param cell The cell
* @returns {boolean} Whether the cell is labeled
*/
function isClustered(cell) {
return cell.cluster !== -1;
}
/**
* Discover the adjacent tiles in all four orthogonal directions, see discoverAdjacentDirection
* @param cell The current cell
*/
function discoverNeighbours(cell) {
discoverAdjacentDirection(cell.row > 0, cell, UP);
discoverAdjacentDirection(cell.row < app.matrix.length - 1, cell, DOWN);
discoverAdjacentDirection(cell.col > 0, cell, LEFT);
discoverAdjacentDirection(cell.col < app.matrix[0].length - 1, cell, RIGHT);
}
/**
* Helper function
* 1. Look if we can go in a direction
* 2. See if it labeled in that direction
* 3. See if it had the same value as the current cell
* 3. TRUE: Add the cell in that direction to the local queue
* 3. FALSE: Add the cell in that direction to the cluster queue
* @param inBounds Check for boundary conditions
* @param cell The current cell
* @param direction The direction
*/
function discoverAdjacentDirection(inBounds, cell, direction) {
if (!inBounds) return;
const nextCell = getDirection(cell, direction);
if (!isClustered(nextCell)) {
// evaluate if the next cell has to be queued
if (isConnected(cell, nextCell) && !nextCell.local_queue) {
nextCell.local_queue = true;
local_queue.push(nextCell);
} else if (!nextCell.global_queue) {
nextCell.global_queue = true;
global_queue.push(nextCell);
}
}
updateQueueSizes();
}
/**
* Helper function to update the queue sizes
*/
function updateQueueSizes() {
app.global_queue_size = global_queue.length;
app.local_queue_size = local_queue.length;
app.max_global_queue_size = Math.max(app.max_global_queue_size, app.global_queue_size);
app.max_local_queue_size = Math.max(app.max_local_queue_size, app.local_queue_size);
}
/**
* Helper function to see if a cell in a direction has the same value as the current one
* @param cell The current cell
* @param nextCell The cell to compare against
* @returns {boolean} If the field in the direction has the same value
*/
function isConnected(cell, nextCell) {
app.comparisons = app.comparisons + 1;
return nextCell.label === cell.label;
}
/**
* Get the cell in a certain direction
* @param cell The current cell
* @param direction The direction
* @returns object The cell
*/
function getDirection(cell, direction) {
let row = cell.row;
let col = cell.col;
switch (direction) {
case UP:
row = row - 1;
break;
case DOWN:
row = row + 1;
break;
case LEFT:
col = col - 1;
break;
case RIGHT:
col = col + 1;
break;
default:
throw Error('Illegal direction');
}
app.lookups = app.lookups + 1;
return app.matrix[row][col];
}
return { generate, play, pause, step };
}