;
- private ctx!: CanvasRenderingContext2D;
- private lastCell: GridPos | null = null;
- isDrawing = false;
+ @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
ngAfterViewInit(): void {
- this.ctx = this.getContextOrThrow();
- this.applyGridSize();
- const el = this.canvas.nativeElement;
- el.addEventListener('mousedown', (e) => this.onMouseDown(e));
- el.addEventListener('mousemove', (e) => this.onMouseMove(e));
- el.addEventListener('mouseup', () => this.onMouseUp());
- el.addEventListener('mouseleave', () => this.onMouseUp());
-
- el.addEventListener('touchstart', (e) => {
- if(e.cancelable) e.preventDefault();
- this.onMouseDown(e as never);
- }, { passive: false });
-
- el.addEventListener('touchmove', (e) => {
- if(e.cancelable) e.preventDefault();
- this.onMouseMove(e as never);
- }, { passive: false });
-
- el.addEventListener('touchend', () => {
- this.onMouseUp();
- });
+ if (this.genericGridComponent) {
+ this.genericGridComponent.initializationFn = this.initializeConwayGrid;
+ this.genericGridComponent.createNodeFn = this.createConwayNode;
+ this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor;
+ this.genericGridComponent.applySelectionFn = this.applyConwaySelection;
+ this.genericGridComponent.gridRows = this.gridRows;
+ this.genericGridComponent.gridCols = this.gridCols;
+ this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
+ this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
+ this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
+ this.genericGridComponent.initializeGrid();
+ }
}
generate(scene: Scenario): void {
this.currentScenario = scene;
- this.initializeGrid();
+ this.genericGridComponent.initializationFn = this.initializeConwayGrid;
+ this.genericGridComponent.initializeGrid();
}
+ applySpeed(): void {
+ this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
+ }
+
+ // --- Callbacks for GenericGridComponent ---
+ createConwayNode = (row: number, col: number): Node => {
+ return {
+ row,
+ col,
+ alive: false
+ };
+ };
+
+ getConwayNodeColor = (node: Node): string => {
+ if (node.alive) {
+ return 'black';
+ }
+ return 'lightgray';
+ };
+
+ applyConwaySelection = (pos: GridPos, grid: Node[][]): void => {
+ this.grid = grid; // Keep internal grid in sync
+ const node = grid[pos.row][pos.col];
+ node.alive = !node.alive; // Toggle alive status
+ };
+
+ initializeConwayGrid = (grid: Node[][]): void => {
+ this.grid = grid;
+ if (this.currentScenario === Scenario.RANDOM) {
+ this.setupRandomLives();
+ }
+ };
+
+ // --- Conway-specific logic (kept local) ---
setupRandomLives(): void {
for (let row = 0; row < this.gridRows; row++) {
for (let col = 0; col < this.gridCols; col++) {
@@ -99,189 +122,7 @@ export class ConwayGol implements AfterViewInit {
}
}
- applyGridSize(): void {
- this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
- this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
- this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
- this.resizeCanvas();
-
- if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
- {
- this.drawGrid();
- return;
- }
- this.initializeGrid();
- }
-
- applySpeed(): void {
- this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
- }
-
-
- private initializeGrid(): void {
- this.grid = this.createEmptyGrid();
- if (this.currentScenario === Scenario.RANDOM) {
- this.setupRandomLives();
- }
-
- this.drawGrid();
- }
-
- private createEmptyGrid(): Node[][] {
- const grid: Node[][] = [];
-
- for (let row = 0; row < this.gridRows; row++) {
- const currentRow: Node[] = [];
- for (let col = 0; col < this.gridCols; col++) {
- currentRow.push(this.createNode(row, col, false));
- }
- grid.push(currentRow);
- }
-
- return grid;
- }
-
- private createNode(row: number, col: number, alive: boolean): Node {
- return {
- row,
- col,
- alive
- };
- }
-
- private drawGrid(): void {
- this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
-
- for (let row = 0; row < this.gridRows; row++) {
- for (let col = 0; col < this.gridCols; col++) {
- this.drawNode(this.grid[row][col]);
- }
- }
- }
-
- private drawNode(node: Node): void {
- this.ctx.fillStyle = this.getNodeColor(node);
- this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
-
- this.ctx.strokeStyle = '#ccc';
- this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
- }
-
- private getNodeColor(node: Node): string {
- if (node.alive)
- {
- return 'black';
- }
- return 'lightgray';
- }
-
- private getContextOrThrow(): CanvasRenderingContext2D {
- const ctx = this.canvas.nativeElement.getContext('2d');
- if (!ctx) {
- throw new Error('CanvasRenderingContext2D not available.');
- }
- return ctx;
- }
-
- private clampGridSize(value: number, fallback: number): number {
- const parsed = Math.floor(Number(value));
- const safe = Number.isFinite(parsed) ? parsed : fallback;
- return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE);
- }
-
- private computeNodeSize(rows: number, cols: number): number {
- const sizeByWidth = Math.floor(MAX_GRID_PX / cols);
- const sizeByHeight = Math.floor(MAX_GRID_PX / rows);
- return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
- }
-
- private resizeCanvas(): void {
- const el = this.canvas.nativeElement;
- el.width = this.gridCols * this.nodeSize;
- el.height = this.gridRows * this.nodeSize;
- }
-
- //mouse listener
- private onMouseDown(event: MouseEvent): void {
- const pos = this.getGridPosition(event);
- if (!pos) {
- return;
- }
-
- this.isDrawing = true;
- this.lastCell = null;
- this.applySelectionAt(pos);
- }
-
- private onMouseMove(event: MouseEvent): void {
- if (!this.isDrawing) {
- return;
- }
-
- const pos = this.getGridPosition(event);
- if (!pos) {
- return;
- }
-
- if (this.isSameCell(pos, this.lastCell)) {
- return;
- }
-
- this.applySelectionAt(pos);
- }
-
- private onMouseUp(): void {
- this.isDrawing = false;
- this.lastCell = null;
- }
-
- // Mouse -> grid cell
- private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
- const canvas = this.canvas.nativeElement;
- const rect = canvas.getBoundingClientRect();
-
- let clientX, clientY;
- if (event instanceof MouseEvent) {
- clientX = event.clientX;
- clientY = event.clientY;
- } else if (event instanceof TouchEvent && event.touches.length > 0) {
- clientX = event.touches[0].clientX;
- clientY = event.touches[0].clientY;
- } else {
- return null;
- }
-
- const scaleX = canvas.width / rect.width;
- const scaleY = canvas.height / rect.height;
-
- const x = (clientX - rect.left) * scaleX;
- const y = (clientY - rect.top) * scaleY;
-
- const col = Math.floor(x / this.nodeSize);
- const row = Math.floor(y / this.nodeSize);
-
- if (!this.isValidPosition(row, col)) {
- return null;
- }
-
- return { row, col };
- }
-
- private isValidPosition(row: number, col: number): boolean {
- return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
- }
-
- private isSameCell(a: GridPos, b: GridPos | null): boolean {
- return !!b && a.row === b.row && a.col === b.col;
- }
-
- private applySelectionAt(pos: GridPos): void {
- const node = this.grid[pos.row][pos.col];
- node.alive = !node.alive;
- this.lastCell = pos;
- this.drawNode(node);
- }
-
+ // --- Other methods ---
protected readonly Scenario = Scenario;
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION;
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html
index ed364a0..36d9ccd 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html
@@ -29,29 +29,25 @@
{{ 'ALGORITHM.GRID_HEIGHT' | translate }}
-
-
+
{{ 'ALGORITHM.GRID_WIDTH' | translate }}
-
-
+
@@ -68,6 +64,16 @@
-
+
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
index 37837e1..98e99ca 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
@@ -1,4 +1,4 @@
-import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
+import {AfterViewInit, Component, inject, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
@@ -15,6 +15,7 @@ import {UrlConstants} from '../../../constants/UrlConstants';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {Information} from '../information/information';
import {AlgorithmInformation} from '../information/information.models';
+import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
enum NodeType {
Start = 'start',
@@ -23,8 +24,6 @@ enum NodeType {
None = 'none'
}
-interface GridPos { row: number; col: number }
-
@Component({
selector: 'app-pathfinding',
standalone: true,
@@ -40,7 +39,8 @@ interface GridPos { row: number; col: number }
MatCardHeader,
MatCardTitle,
MatCardContent,
- Information
+ Information,
+ GenericGridComponent
],
templateUrl: './pathfinding.component.html',
})
@@ -51,6 +51,7 @@ export class PathfindingComponent implements AfterViewInit {
readonly NodeType = NodeType;
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
+ readonly MAX_GRID_PX = MAX_GRID_PX;
algoInformation: AlgorithmInformation = {
title: 'PATHFINDING.EXPLANATION.TITLE',
@@ -71,24 +72,15 @@ export class PathfindingComponent implements AfterViewInit {
disclaimerListEntry: []
};
- @ViewChild('gridCanvas', { static: true })
- canvas!: ElementRef;
-
- private ctx!: CanvasRenderingContext2D;
-
gridRows = DEFAULT_GRID_ROWS;
gridCols = DEFAULT_GRID_COLS;
- nodeSize = 10;
grid: Node[][] = [];
startNode: Node | null = null;
endNode: Node | null = null;
selectedNodeType: NodeType = NodeType.None;
-
- isDrawing = false;
- private lastCell: GridPos | null = null;
- private shouldAddWall = true;
+ private shouldAddWall = true; // Moved here
animationSpeed = 3;
pathLength = "0";
@@ -96,58 +88,79 @@ export class PathfindingComponent implements AfterViewInit {
private timeoutIds: number[] = [];
+ @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
+
ngAfterViewInit(): void {
- this.ctx = this.getContextOrThrow();
- this.applyGridSize(true);
-
- const el = this.canvas.nativeElement;
- el.addEventListener('mousedown', (e) => this.onMouseDown(e));
- el.addEventListener('mousemove', (e) => this.onMouseMove(e));
- el.addEventListener('mouseup', () => this.onMouseUp());
- el.addEventListener('mouseleave', () => this.onMouseUp());
-
- el.addEventListener('touchstart', (e) => {
- if(e.cancelable) e.preventDefault();
- this.onMouseDown(e as never);
- }, { passive: false });
-
- el.addEventListener('touchmove', (e) => {
- if(e.cancelable) e.preventDefault();
- this.onMouseMove(e as never);
- }, { passive: false });
-
- el.addEventListener('touchend', () => {
- this.onMouseUp();
- });
+ // Canvas logic is now handled by GenericGridComponent
+ // Ensure genericGridComponent is initialized
+ if (this.genericGridComponent) {
+ this.genericGridComponent.initializationFn = this.initializePathfindingGrid;
+ this.genericGridComponent.createNodeFn = this.createPathfindingNode;
+ this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor;
+ this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection;
+ this.genericGridComponent.gridRows = this.gridRows;
+ this.genericGridComponent.gridCols = this.gridCols;
+ this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
+ this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
+ this.genericGridComponent.maxGridPx = MAX_GRID_PX;
+ this.genericGridComponent.applyGridSize(); // Trigger initial grid setup
+ }
+ this.createCase({withWalls: true, scenario: "normal"});
}
- applyGridSize(skipReset?: boolean): void {
- this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
- this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
- this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
- this.resizeCanvas();
+ // --- Callbacks for GenericGridComponent ---
+ createPathfindingNode = (row: number, col: number): Node => {
+ return {
+ row,
+ col,
+ isStart: false,
+ isEnd: false,
+ isWall: false,
+ isVisited: false,
+ isPath: false,
+ distance: Infinity,
+ previousNode: null,
+ hScore: 0,
+ fScore: Infinity,
+ };
+ };
- if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
- {
- this.drawGrid();
- return;
+ getPathfindingNodeColor = (node: Node): string => {
+ if (node.isStart) return 'green';
+ if (node.isEnd) return 'red';
+ if (node.isPath) return 'gold';
+ if (node.isVisited) return 'skyblue';
+ if (node.isWall) return 'black';
+ return 'lightgray';
+ };
+
+ applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => {
+ this.grid = grid; // Keep internal grid in sync
+ const node = grid[pos.row][pos.col];
+
+ // Determine if we should add or remove a wall
+ if (this.selectedNodeType === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) {
+ this.shouldAddWall = !node.isWall;
}
- if (skipReset) {
- this.initializeGrid({withWalls: true, scenario: 'normal'});
- this.drawGrid();
- return;
+ switch (this.selectedNodeType) {
+ case NodeType.Start:
+ this.trySetStart(node);
+ break;
+
+ case NodeType.End:
+ this.trySetEnd(node);
+ break;
+
+ case NodeType.Wall:
+ this.tryToggleWall(node, this.shouldAddWall);
+ break;
+
+ case NodeType.None:
+ this.tryClearNode(node);
+ break;
}
-
- this.createCase({withWalls: true, scenario: 'normal'});
- }
-
- createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void
- {
- this.stopAnimations();
- this.initializeGrid({withWalls, scenario});
- this.drawGrid();
- }
+ };
visualize(algorithm: string): void {
if (!this.ensureStartAndEnd()) {
@@ -166,13 +179,13 @@ export class PathfindingComponent implements AfterViewInit {
this.grid[this.startNode!.row][this.startNode!.col],
this.grid[this.endNode!.row][this.endNode!.col]
);
- break;
+ break;
case 'astar': result = this.pathfindingService.aStar(
this.grid,
this.grid[this.startNode!.row][this.startNode!.col],
this.grid[this.endNode!.row][this.endNode!.col]
);
- break;
+ break;
}
if (!result)
@@ -195,320 +208,19 @@ export class PathfindingComponent implements AfterViewInit {
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
}
- // Mouse interactions
- private onMouseDown(event: MouseEvent): void {
- this.stopAnimations();
- this.clearPath();
- const pos = this.getGridPosition(event);
- if (!pos) {
- return;
- }
-
- this.shouldAddWall = this.shouldStartWallStroke(pos);
-
- this.isDrawing = true;
- this.lastCell = null;
- this.applySelectionAt(pos);
- }
-
- private onMouseMove(event: MouseEvent): void {
- if (!this.isDrawing) {
- return;
- }
-
- const pos = this.getGridPosition(event);
- if (!pos) {
- return;
- }
-
- if (this.isSameCell(pos, this.lastCell)) {
- return;
- }
-
- this.applySelectionAt(pos);
- }
-
- private onMouseUp(): void {
- this.isDrawing = false;
- this.lastCell = null;
- }
-
- private applySelectionAt(pos: GridPos): void {
- const node = this.grid[pos.row][pos.col];
-
- switch (this.selectedNodeType) {
- case NodeType.Start:
- this.trySetStart(node);
- break;
-
- case NodeType.End:
- this.trySetEnd(node);
- break;
-
- case NodeType.Wall:
- this.tryToggleWall(node, this.shouldAddWall);
- break;
-
- case NodeType.None:
- this.tryClearNode(node);
- break;
- }
-
- this.lastCell = pos;
- this.drawNode(node);
- }
-
- // Grid init
- private initializeGrid({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
- this.grid = this.createEmptyGrid();
-
- const { start, end } = this.getScenarioStartEnd(scenario);
+ initializePathfindingGrid = (grid: Node[][]): void => {
+ this.grid = grid; // Update the component's grid reference
+ const {start, end} = this.getScenarioStartEnd('normal'); // Default scenario
this.startNode = this.grid[start.row][start.col];
this.endNode = this.grid[end.row][end.col];
this.startNode.isStart = true;
this.endNode.isEnd = true;
- if (withWalls) {
- this.placeDefaultDiagonalWall(scenario);
- }
- }
+ this.placeDefaultDiagonalWall('normal');
+ };
- private createEmptyGrid(): Node[][] {
- const grid: Node[][] = [];
-
- for (let row = 0; row < this.gridRows; row++) {
- const currentRow: Node[] = [];
- for (let col = 0; col < this.gridCols; col++) {
- currentRow.push(this.createNode(row, col));
- }
- grid.push(currentRow);
- }
-
- return grid;
- }
-
- private createNode(row: number, col: number): Node {
- return {
- row,
- col,
- isStart: false,
- isEnd: false,
- isWall: false,
- isVisited: false,
- isPath: false,
- distance: Infinity,
- previousNode: null,
- hScore: 0,
- fScore: Infinity,
- };
- }
-
- private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
- if (scenario === 'edge') {
- return {
- start: { row: 0, col: 0 },
- end: { row: this.gridRows - 1, col: this.gridCols - 1 }
- };
- }
- else if (scenario === 'random') {
- return this.createRandomStartEndPosition();
- }
- else {
- // normal: mid-left -> mid-right
- const midRow = Math.floor(this.gridRows / 2);
- return {
- start: { row: midRow, col: 0 },
- end: { row: midRow, col: this.gridCols - 1 }
- };
- }
- }
-
- private createRandomStartEndPosition() {
- const midCol = Math.floor(this.gridCols / 2);
-
- const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
- const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1);
-
- const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
- let endCol: number;
-
- if (startCol <= midCol) {
- endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1);
- } else {
- endCol = this.randomIntFromInterval(0, midCol);
- }
-
- return {
- start: {row: startRow, col: startCol},
- end: {row: endRow, col: endCol}
- };
- }
-
- private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void {
- if (scenario === 'edge') {
- this.createDiagonalWall();
- }
- else if (scenario === 'normal') {
- this.createVerticalWall();
- }
- else if (scenario === 'random') {
- this.createRandomWalls();
- }
- }
-
- private createRandomWalls(){
- const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows);
-
- for (let wall = 0; wall < maxNumberOfWalls; wall++) {
-
- const row: number = this.randomIntFromInterval(0, this.gridRows - 1);
- const col: number = this.randomIntFromInterval(0, this.gridCols - 1);
-
- if (!this.isValidPosition(row, col)) {
- wall--;
- continue;
- }
-
- const node = this.grid[row][col];
- if (node.isStart || node.isEnd) {
- wall--;
- continue;
- }
-
- node.isWall = true;
- }
-
- }
-
- private createVerticalWall() {
- const height = this.gridRows;
- const startCol = Math.floor(this.gridCols / 2);
-
- for (let i = 5; i < (height - 5); i++) {
- const row = i;
-
- if (!this.isValidPosition(row, startCol)) {
- continue;
- }
-
- const node = this.grid[row][startCol];
- if (node.isStart || node.isEnd) {
- continue;
- }
-
- node.isWall = true;
- }
-
- }
-
- private createDiagonalWall() {
- // Diagonal-ish wall; avoids start/end
- const len = Math.min(this.gridRows, this.gridCols);
- const startCol = Math.floor((this.gridCols - len) / 2);
-
- for (let i = 0; i < Math.max(0, len - 10); i++) {
- const row = len - i - 1;
- const col = startCol + i;
-
- if (!this.isValidPosition(row, col)) {
- continue;
- }
-
- const node = this.grid[row][col];
- if (node.isStart || node.isEnd) {
- continue;
- }
-
- node.isWall = true;
- }
- }
-
-// Path state
- private clearPath(): void {
- for (let row = 0; row < this.gridRows; row++) {
- for (let col = 0; col < this.gridCols; col++) {
- const node = this.grid[row][col];
- node.isVisited = false;
- node.isPath = false;
- node.distance = Infinity;
- node.previousNode = null;
- }
- }
- this.drawGrid();
- }
-
- // Animation
- private stopAnimations(): void {
- for (const id of this.timeoutIds) {
- clearTimeout(id);
- }
- this.timeoutIds = [];
- }
-
- private animateAlgorithm(visited: Node[], path: Node[]): void {
- for (let i = 0; i <= visited.length; i++) {
- if (i === visited.length) {
- const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
- this.timeoutIds.push(id);
- return;
- }
-
- const node = visited[i];
- const id = globalThis.setTimeout(() => {
- if (!node.isStart && !node.isEnd) {
- node.isVisited = true;
- this.drawNode(node);
- }
- }, this.animationSpeed * i);
-
- this.timeoutIds.push(id);
- }
- }
-
- private animateShortestPath(path: Node[]): void {
- for (let i = 0; i < path.length; i++) {
- const node = path[i];
- const id = globalThis.setTimeout(() => {
- if (!node.isStart && !node.isEnd) {
- node.isPath = true;
- this.drawNode(node);
- }
- }, this.animationSpeed * i);
-
- this.timeoutIds.push(id);
- }
- }
-
- // Drawing
- private drawGrid(): void {
- this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
-
- for (let row = 0; row < this.gridRows; row++) {
- for (let col = 0; col < this.gridCols; col++) {
- this.drawNode(this.grid[row][col]);
- }
- }
- }
-
- private drawNode(node: Node): void {
- this.ctx.fillStyle = this.getNodeColor(node);
- this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
-
- this.ctx.strokeStyle = '#ccc';
- this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
- }
-
- private getNodeColor(node: Node): string {
- if (node.isStart) return 'green';
- if (node.isEnd) return 'red';
- if (node.isPath) return 'gold';
- if (node.isVisited) return 'skyblue';
- if (node.isWall) return 'black';
- return 'lightgray';
- }
-
- // Placement rules (readability helpers)
+ // --- Helper methods for node manipulation (kept local) ---
private trySetStart(node: Node): void {
if (!this.canBeStart(node)) {
return;
@@ -516,7 +228,7 @@ export class PathfindingComponent implements AfterViewInit {
if (this.startNode) {
this.startNode.isStart = false;
- this.drawNode(this.startNode);
+ this.genericGridComponent.drawNode(this.startNode); // Redraw old start node
}
node.isStart = true;
@@ -530,7 +242,7 @@ export class PathfindingComponent implements AfterViewInit {
if (this.endNode) {
this.endNode.isEnd = false;
- this.drawNode(this.endNode);
+ this.genericGridComponent.drawNode(this.endNode); // Redraw old end node
}
node.isEnd = true;
@@ -574,16 +286,197 @@ export class PathfindingComponent implements AfterViewInit {
return !node.isStart && !node.isEnd;
}
- private shouldStartWallStroke(pos: GridPos): boolean {
- if (this.selectedNodeType !== NodeType.Wall) {
- return true;
- }
+ // --- Grid manipulation for scenarios (kept local) ---
+ createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
+ this.stopAnimations();
+ // Reinitialize grid through the generic component
+ this.genericGridComponent.initializationFn = (grid) => {
+ this.grid = grid;
+ const {start, end} = this.getScenarioStartEnd(scenario);
+ this.startNode = this.grid[start.row][start.col];
+ this.endNode = this.grid[end.row][end.col];
+ this.startNode.isStart = true;
+ this.endNode.isEnd = true;
- const node = this.grid[pos.row][pos.col];
- return !node.isWall;
+ if (withWalls) {
+ this.placeDefaultDiagonalWall(scenario);
+ }
+ };
+ this.genericGridComponent.initializeGrid(); // Trigger re-initialization and redraw
}
- // Validation
+ private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
+ if (scenario === 'edge') {
+ return {
+ start: {row: 0, col: 0},
+ end: {row: this.gridRows - 1, col: this.gridCols - 1}
+ };
+ } else if (scenario === 'random') {
+ return this.createRandomStartEndPosition();
+ } else {
+ // normal: mid-left -> mid-right
+ const midRow = Math.floor(this.gridRows / 2);
+ return {
+ start: {row: midRow, col: 0},
+ end: {row: midRow, col: this.gridCols - 1}
+ };
+ }
+ }
+
+ private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
+ const midCol = Math.floor(this.gridCols / 2);
+
+ const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
+ const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1);
+
+ const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
+ let endCol: number;
+
+ if (startCol <= midCol) {
+ endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1);
+ } else {
+ endCol = this.randomIntFromInterval(0, midCol);
+ }
+
+ return {
+ start: {row: startRow, col: startCol},
+ end: {row: endRow, col: endCol}
+ };
+ }
+
+ private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void {
+ if (scenario === 'edge') {
+ this.createDiagonalWall();
+ } else if (scenario === 'normal') {
+ this.createVerticalWall();
+ } else if (scenario === 'random') {
+ this.createRandomWalls();
+ }
+ }
+
+ private createRandomWalls() {
+ const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows);
+
+ for (let wall = 0; wall < maxNumberOfWalls; wall++) {
+
+ const row: number = this.randomIntFromInterval(0, this.gridRows - 1);
+ const col: number = this.randomIntFromInterval(0, this.gridCols - 1);
+
+ if (!this.grid[row][col]) { // Use the grid passed from GenericGrid
+ wall--;
+ continue;
+ }
+
+ const node = this.grid[row][col];
+ if (node.isStart || node.isEnd) {
+ wall--;
+ continue;
+ }
+
+ node.isWall = true;
+ }
+
+ }
+
+ private createVerticalWall() {
+ const height = this.gridRows;
+ const startCol = Math.floor(this.gridCols / 2);
+
+ for (let i = 5; i < (height - 5); i++) {
+ const row = i;
+
+ if (!this.grid[row]?.[startCol]) {
+ continue;
+ }
+
+ const node = this.grid[row][startCol];
+ if (node.isStart || node.isEnd) {
+ continue;
+ }
+
+ node.isWall = true;
+ }
+
+ }
+
+ private createDiagonalWall() {
+ // Diagonal-ish wall; avoids start/end
+ const len = Math.min(this.gridRows, this.gridCols);
+ const startCol = Math.floor((this.gridCols - len) / 2);
+
+ for (let i = 0; i < Math.max(0, len - 10); i++) {
+ const row = len - i - 1;
+ const col = startCol + i;
+
+ if (!this.grid[row]?.[col]) {
+ continue;
+ }
+
+ const node = this.grid[row][col];
+ if (node.isStart || node.isEnd) {
+ continue;
+ }
+
+ node.isWall = true;
+ }
+ }
+
+ // --- Animation (adapted to use genericGridComponent for redraw) ---
+ private stopAnimations(): void {
+ for (const id of this.timeoutIds) {
+ clearTimeout(id);
+ }
+ this.timeoutIds = [];
+ }
+
+ private clearPath(): void {
+ for (let row = 0; row < this.gridRows; row++) {
+ for (let col = 0; col < this.gridCols; col++) {
+ const node = this.grid[row][col];
+ node.isVisited = false;
+ node.isPath = false;
+ node.distance = Infinity;
+ node.previousNode = null;
+ }
+ }
+ this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
+ }
+
+ private animateAlgorithm(visited: Node[], path: Node[]): void {
+ for (let i = 0; i <= visited.length; i++) {
+ if (i === visited.length) {
+ const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
+ this.timeoutIds.push(id);
+ return;
+ }
+
+ const node = visited[i];
+ const id = globalThis.setTimeout(() => {
+ if (!node.isStart && !node.isEnd) {
+ node.isVisited = true;
+ this.genericGridComponent?.drawNode(node); // Redraw single node
+ }
+ }, this.animationSpeed * i);
+
+ this.timeoutIds.push(id);
+ }
+ }
+
+ private animateShortestPath(path: Node[]): void {
+ for (let i = 0; i < path.length; i++) {
+ const node = path[i];
+ const id = globalThis.setTimeout(() => {
+ if (!node.isStart && !node.isEnd) {
+ node.isPath = true;
+ this.genericGridComponent?.drawNode(node); // Redraw single node
+ }
+ }, this.animationSpeed * i);
+
+ this.timeoutIds.push(id);
+ }
+ }
+
+ // --- Validation ---
private ensureStartAndEnd(): boolean {
if (this.startNode && this.endNode) {
return true;
@@ -593,73 +486,7 @@ export class PathfindingComponent implements AfterViewInit {
return false;
}
- // Grid sizing
- private clampGridSize(value: number, fallback: number): number {
- const parsed = Math.floor(Number(value));
- const safe = Number.isFinite(parsed) ? parsed : fallback;
- return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE);
- }
-
- private computeNodeSize(rows: number, cols: number): number {
- const sizeByWidth = Math.floor(MAX_GRID_PX / cols);
- const sizeByHeight = Math.floor(MAX_GRID_PX / rows);
- return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
- }
-
- private resizeCanvas(): void {
- const el = this.canvas.nativeElement;
- el.width = this.gridCols * this.nodeSize;
- el.height = this.gridRows * this.nodeSize;
- }
-
- // Mouse -> grid cell
- private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
- const canvas = this.canvas.nativeElement;
- const rect = canvas.getBoundingClientRect();
-
- let clientX, clientY;
- if (event instanceof MouseEvent) {
- clientX = event.clientX;
- clientY = event.clientY;
- } else if (event instanceof TouchEvent && event.touches.length > 0) {
- clientX = event.touches[0].clientX;
- clientY = event.touches[0].clientY;
- } else {
- return null;
- }
-
- const scaleX = canvas.width / rect.width;
- const scaleY = canvas.height / rect.height;
-
- const x = (clientX - rect.left) * scaleX;
- const y = (clientY - rect.top) * scaleY;
-
- const col = Math.floor(x / this.nodeSize);
- const row = Math.floor(y / this.nodeSize);
-
- if (!this.isValidPosition(row, col)) {
- return null;
- }
-
- return { row, col };
- }
-
- private isValidPosition(row: number, col: number): boolean {
- return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
- }
-
- private isSameCell(a: GridPos, b: GridPos | null): boolean {
- return !!b && a.row === b.row && a.col === b.col;
- }
-
- private getContextOrThrow(): CanvasRenderingContext2D {
- const ctx = this.canvas.nativeElement.getContext('2d');
- if (!ctx) {
- throw new Error('CanvasRenderingContext2D not available.');
- }
- return ctx;
- }
-
+ // --- Utility ---
private randomIntFromInterval(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min);
}
diff --git a/src/app/shared/components/generic-grid/generic-grid.html b/src/app/shared/components/generic-grid/generic-grid.html
new file mode 100644
index 0000000..d0e9eb5
--- /dev/null
+++ b/src/app/shared/components/generic-grid/generic-grid.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app/shared/components/generic-grid/generic-grid.scss b/src/app/shared/components/generic-grid/generic-grid.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/shared/components/generic-grid/generic-grid.ts b/src/app/shared/components/generic-grid/generic-grid.ts
new file mode 100644
index 0000000..55c8035
--- /dev/null
+++ b/src/app/shared/components/generic-grid/generic-grid.ts
@@ -0,0 +1,213 @@
+import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {CommonModule} from '@angular/common';
+
+export interface GridPos { row: number; col: number }
+
+@Component({
+ selector: 'app-generic-grid',
+ standalone: true,
+ imports: [CommonModule],
+ templateUrl: './generic-grid.html',
+ styleUrl: './generic-grid.scss',
+})
+export class GenericGridComponent implements AfterViewInit {
+ @ViewChild('gridCanvas', { static: true })
+ canvas!: ElementRef;
+
+ @Input() gridRows: number = 10;
+ @Input() gridCols: number = 10;
+ @Input() nodeSize: number = 10; // Default node size, can be overridden by computeNodeSize
+ @Input() maxGridPx: number = 500; // Max pixels for grid dimension
+ @Input() minGridSize: number = 5;
+ @Input() maxGridSize: number = 50;
+ @Input() drawNodeBorderColor: string = '#ccc';
+
+ // Callbacks from parent component
+ @Input() createNodeFn!: (row: number, col: number) => any;
+ @Input() getNodeColorFn!: (node: any) => string;
+ @Input() applySelectionFn!: (pos: GridPos, grid: any[][]) => void;
+ @Input() initializationFn!: (grid: any[][]) => void;
+
+ @Output() gridChange = new EventEmitter();
+ @Output() nodeClick = new EventEmitter();
+
+ private ctx!: CanvasRenderingContext2D;
+ grid: any[][] = [];
+
+ isDrawing = false;
+ private lastCell: GridPos | null = null;
+
+ ngAfterViewInit(): void {
+ this.ctx = this.getContextOrThrow();
+ this.setupCanvasListeners();
+ this.applyGridSize();
+ }
+
+ setupCanvasListeners(): void {
+ const el = this.canvas.nativeElement;
+ el.addEventListener('mousedown', (e) => this.onMouseDown(e));
+ el.addEventListener('mousemove', (e) => this.onMouseMove(e));
+ el.addEventListener('mouseup', () => this.onMouseUp());
+ el.addEventListener('mouseleave', () => this.onMouseUp());
+
+ el.addEventListener('touchstart', (e) => {
+ if (e.cancelable) e.preventDefault();
+ this.onMouseDown(e as never);
+ }, { passive: false });
+
+ el.addEventListener('touchmove', (e) => {
+ if (e.cancelable) e.preventDefault();
+ this.onMouseMove(e as never);
+ }, { passive: false });
+
+ el.addEventListener('touchend', () => {
+ this.onMouseUp();
+ });
+ }
+
+ applyGridSize(): void {
+ this.gridRows = this.clampGridSize(this.gridRows);
+ this.gridCols = this.clampGridSize(this.gridCols);
+ this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
+ this.resizeCanvas();
+ if (this.gridRows === this.grid.length && this.gridCols === this.grid[0]?.length) {
+ this.drawGrid();
+ return;
+ }
+ this.initializeGrid();
+ }
+
+ initializeGrid(): void {
+ this.grid = this.createEmptyGrid();
+ if (this.initializationFn) {
+ this.initializationFn(this.grid);
+ }
+ this.drawGrid();
+ this.gridChange.emit(this.grid);
+ }
+
+ createEmptyGrid(): any[][] {
+ const grid: any[][] = [];
+ for (let row = 0; row < this.gridRows; row++) {
+ const currentRow: any[] = [];
+ for (let col = 0; col < this.gridCols; col++) {
+ currentRow.push(this.createNodeFn(row, col));
+ }
+ grid.push(currentRow);
+ }
+ return grid;
+ }
+
+ drawGrid(): void {
+ this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
+ for (let row = 0; row < this.gridRows; row++) {
+ for (let col = 0; col < this.gridCols; col++) {
+ this.drawNode(this.grid[row][col]);
+ }
+ }
+ }
+
+ drawNode(node: any): void {
+ this.ctx.fillStyle = this.getNodeColorFn(node);
+ this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
+ this.ctx.strokeStyle = this.drawNodeBorderColor;
+ this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
+ }
+
+ private getContextOrThrow(): CanvasRenderingContext2D {
+ const ctx = this.canvas.nativeElement.getContext('2d');
+ if (!ctx) {
+ throw new Error('CanvasRenderingContext2D not available.');
+ }
+ return ctx;
+ }
+
+ private clampGridSize(value: number): number {
+ const parsed = Math.floor(Number(value));
+ const safe = Number.isFinite(parsed) ? parsed : this.minGridSize; // Use minGridSize as fallback
+ return Math.min(Math.max(this.minGridSize, safe), this.maxGridSize);
+ }
+
+ private computeNodeSize(rows: number, cols: number): number {
+ const sizeByWidth = Math.floor(this.maxGridPx / cols);
+ const sizeByHeight = Math.floor(this.maxGridPx / rows);
+ return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
+ }
+
+ private resizeCanvas(): void {
+ const el = this.canvas.nativeElement;
+ el.width = this.gridCols * this.nodeSize;
+ el.height = this.gridRows * this.nodeSize;
+ }
+
+ onMouseDown(event: MouseEvent | TouchEvent): void {
+ this.isDrawing = true;
+ this.lastCell = null;
+ const pos = this.getGridPosition(event);
+ if (pos) {
+ this.handleInteraction(pos);
+ }
+ }
+
+ onMouseMove(event: MouseEvent | TouchEvent): void {
+ if (!this.isDrawing) {
+ return;
+ }
+ const pos = this.getGridPosition(event);
+ if (pos && !this.isSameCell(pos, this.lastCell)) {
+ this.handleInteraction(pos);
+ }
+ }
+
+ onMouseUp(): void {
+ this.isDrawing = false;
+ this.lastCell = null;
+ }
+
+ private handleInteraction(pos: GridPos): void {
+ this.applySelectionFn(pos, this.grid);
+ this.drawNode(this.grid[pos.row][pos.col]);
+ this.lastCell = pos;
+ this.nodeClick.emit(pos);
+ this.gridChange.emit(this.grid);
+ }
+
+ private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
+ const canvas = this.canvas.nativeElement;
+ const rect = canvas.getBoundingClientRect();
+
+ let clientX, clientY;
+ if (event instanceof MouseEvent) {
+ clientX = event.clientX;
+ clientY = event.clientY;
+ } else if (event instanceof TouchEvent && event.touches.length > 0) {
+ clientX = event.touches[0].clientX;
+ clientY = event.touches[0].clientY;
+ } else {
+ return null;
+ }
+
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+
+ const x = (clientX - rect.left) * scaleX;
+ const y = (clientY - rect.top) * scaleY;
+
+ const col = Math.floor(x / this.nodeSize);
+ const row = Math.floor(y / this.nodeSize);
+
+ if (!this.isValidPosition(row, col)) {
+ return null;
+ }
+
+ return { row, col };
+ }
+
+ private isValidPosition(row: number, col: number): boolean {
+ return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
+ }
+
+ private isSameCell(a: GridPos, b: GridPos | null): boolean {
+ return !!b && a.row === b.row && a.col === b.col;
+ }
+}