@for (category of categories$ | async; track category.id) {
- {{ category.title }}
+ {{ category.title | translate }}
- {{ category.description }}
+ {{ category.description | translate}}
}
diff --git a/src/app/pages/algorithms/algorithms.component.scss b/src/app/pages/algorithms/algorithms.component.scss
index d6d57ce..825e521 100644
--- a/src/app/pages/algorithms/algorithms.component.scss
+++ b/src/app/pages/algorithms/algorithms.component.scss
@@ -10,10 +10,7 @@
mat-card {
cursor: pointer;
- max-width: 300px;
-
- &:hover {
- background-color: rgba(255, 255, 255, 0.1);
- }
+ min-width: 300px;
+ max-width: 400px;
}
}
diff --git a/src/app/pages/algorithms/algorithms.component.ts b/src/app/pages/algorithms/algorithms.component.ts
index 66b7540..8639131 100644
--- a/src/app/pages/algorithms/algorithms.component.ts
+++ b/src/app/pages/algorithms/algorithms.component.ts
@@ -5,22 +5,20 @@ import { Observable } from 'rxjs';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
+import {TranslatePipe} from '@ngx-translate/core';
@Component({
selector: 'app-algorithms',
templateUrl: './algorithms.component.html',
styleUrls: ['./algorithms.component.scss'],
standalone: true,
- imports: [CommonModule, RouterLink, MatCardModule],
+ imports: [CommonModule, RouterLink, MatCardModule, TranslatePipe],
})
export class AlgorithmsComponent implements OnInit {
- private algorithmsService = inject(AlgorithmsService);
-
+ private readonly algorithmsService = inject(AlgorithmsService);
categories$: Observable
| undefined;
-
-
ngOnInit(): void {
this.categories$ = this.algorithmsService.getCategories();
}
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html
index 2355c4c..6395d82 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html
@@ -1,6 +1,20 @@
{{ 'PATHFINDING.TITLE' | translate }}
+
+
{{ 'PATHFINDING.EXPLANATION.TITLE' | translate }}
+
+
+ Dijkstra {{ 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION' | translate }}
+ Wikipedia
+
+
+
+ A* {{ 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION' | translate}}
+ Wikipedia
+
+
+
@@ -9,11 +23,44 @@
{{ 'PATHFINDING.WALL' | translate }}
{{ 'PATHFINDING.CLEAR_NODE' | translate }}
+
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -25,10 +72,10 @@
-
-
{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}
{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms
+
+
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss
index 94346e3..070efd4 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss
@@ -2,6 +2,25 @@
padding: 2rem;
}
+.algo-info {
+ margin: 0 0 1rem 0;
+ padding: 0.75rem 1rem;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+
+ h3 {
+ margin: 0 0 0.5rem 0;
+ }
+
+ p {
+ margin: 0.5rem 0;
+ }
+
+ a {
+ margin-left: 0.25rem;
+ }
+}
+
.controls-container {
display: flex;
flex-direction: column;
@@ -13,6 +32,7 @@
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
+ align-items: center;
mat-button-toggle-group {
border-radius: 4px;
@@ -20,6 +40,17 @@
}
}
+.grid-size {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.grid-field {
+ width: 150px;
+}
+
.legend {
display: flex;
flex-wrap: wrap;
@@ -46,4 +77,5 @@
canvas {
border: 1px solid #ccc;
display: block;
-}
\ No newline at end of file
+ max-width: 100%;
+}
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
index 90ed8bd..bbd8b49 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
@@ -1,13 +1,18 @@
-import { AfterViewInit, Component, ElementRef, ViewChild, inject } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { MatButtonModule } from '@angular/material/button';
-import {GRID_COLS, GRID_ROWS, NODE_SIZE, Node} from './pathfinding.models';
-import {MatButtonToggleModule} from '@angular/material/button-toggle';
+import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
+import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
-import { PathfindingService } from './service/pathfinding.service';
-import { TranslateModule, TranslateService } from '@ngx-translate/core';
-// Define an enum for node types that can be placed by the user
+import {MatButtonModule} from '@angular/material/button';
+import {MatButtonToggleModule} from '@angular/material/button-toggle';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatInputModule} from '@angular/material/input';
+
+import {TranslateModule, TranslateService} from '@ngx-translate/core';
+
+import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, Node} from './pathfinding.models';
+import {PathfindingService} from './service/pathfinding.service';
+import {UrlConstants} from '../../../constants/UrlConstants';
+
enum NodeType {
Start = 'start',
End = 'end',
@@ -15,334 +20,295 @@ enum NodeType {
None = 'none'
}
+interface GridPos { row: number; col: number }
+
@Component({
selector: 'app-pathfinding',
standalone: true,
- imports: [CommonModule, MatButtonModule, MatButtonToggleModule, FormsModule, TranslateModule],
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatButtonModule,
+ MatButtonToggleModule,
+ MatFormFieldModule,
+ MatInputModule,
+ TranslateModule
+ ],
templateUrl: './pathfinding.component.html',
styleUrls: ['./pathfinding.component.scss']
})
export class PathfindingComponent implements AfterViewInit {
private readonly pathfindingService = inject(PathfindingService);
private readonly translate = inject(TranslateService);
- private lastRow = -1;
- private lastCol = -1;
- private timeoutIds: any[] = [];
+
+ readonly NodeType = NodeType;
+ readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
+ readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
@ViewChild('gridCanvas', { static: true })
canvas!: ElementRef;
- ctx!: CanvasRenderingContext2D;
+
+ 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;
- shouldAddWall = true;
- selectedNodeType: NodeType = NodeType.None; // Default to no selection
- animationSpeed = 3; // milliseconds
+ private lastCell: GridPos | null = null;
+ private shouldAddWall = true;
+
+ animationSpeed = 3;
pathLength = 0;
executionTime = 0;
-
- readonly NodeType = NodeType;
+ private timeoutIds: number[] = [];
ngAfterViewInit(): void {
- this.ctx = this.canvas.nativeElement.getContext('2d') as CanvasRenderingContext2D;
- this.canvas.nativeElement.width = GRID_COLS * NODE_SIZE;
- this.canvas.nativeElement.height = GRID_ROWS * NODE_SIZE;
- this.initializeGrid(true);
+ 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());
+ }
+
+ 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();
+
+ if (skipReset) {
+ this.initializeGrid(true, 'edge');
+ this.drawGrid();
+ return;
+ }
+
+ // Default after size changes: pick one consistent scenario
+ this.edgeCase();
+ }
+
+ // Scenarios (buttons)
+ normalCase(): void {
+ this.stopAnimations();
+ this.initializeGrid(true, 'normal');
this.drawGrid();
-
- // Add event listeners for mouse interactions
- this.canvas.nativeElement.addEventListener('mousedown', this.onMouseDown.bind(this));
- this.canvas.nativeElement.addEventListener('mousemove', this.onMouseMove.bind(this));
- this.canvas.nativeElement.addEventListener('mouseup', this.onMouseUp.bind(this));
- this.canvas.nativeElement.addEventListener('mouseleave', this.onMouseUp.bind(this)); // Stop drawing if mouse leaves canvas
}
- initializeGrid(withWalls: boolean): void {
- this.grid = [];
- for (let row = 0; row < GRID_ROWS; row++) {
- const currentRow: Node[] = [];
- for (let col = 0; col < GRID_COLS; col++) {
- currentRow.push({
- row,
- col,
- isStart: false,
- isEnd: false,
- isWall: false,
- isVisited: false,
- isPath: false,
- distance: Infinity,
- previousNode: null,
- fScore: 0
- });
- }
- this.grid.push(currentRow);
- }
-
- // Set default start and end nodes
- this.startNode = this.grid[0][Math.floor(GRID_COLS / 2)];
- this.startNode.isStart = true;
- this.endNode = this.grid[this.grid.length-1][Math.floor(GRID_COLS / 2)];
- this.endNode.isEnd = true;
-
- if (withWalls)
- {
- //setting walls
- let offset = Math.floor(GRID_COLS / 4);
- for (let startWall = 0; startWall < Math.floor(GRID_COLS /2 ); startWall++){
- this.grid[Math.floor(GRID_ROWS / 2)][offset + startWall].isWall = true;
- }
- }
- }
-
- stopAnimations(): void {
- this.timeoutIds.forEach((id) => clearTimeout(id));
- this.timeoutIds = [];
- }
-
- drawGrid(): void {
- if (!this.ctx) {
- return;
- }
-
- this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
-
- for (let row = 0; row < GRID_ROWS; row++) {
- for (let col = 0; col < GRID_COLS; col++) {
- const node = this.grid[row][col];
- let color = 'lightgray'; // Default color
-
- if (node.isStart) {
- color = 'green';
- } else if (node.isEnd) {
- color = 'red';
- } else if (node.isPath) {
- color = 'gold';
- } else if (node.isVisited) {
- color = 'skyblue';
- } else if (node.isWall) {
- color = 'black';
- }
-
- this.ctx.fillStyle = color;
- this.ctx.fillRect(col * NODE_SIZE, row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
- this.ctx.strokeStyle = '#ccc';
- this.ctx.strokeRect(col * NODE_SIZE, row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
- }
- }
- }
-
- onMouseDown(event: MouseEvent): void {
- const { row, col } = this.getGridPosition(event);
-
- if (this.isValidPosition(row, col)) {
- const node = this.grid[row][col];
- this.shouldAddWall = !node.isWall;
- }
-
- this.isDrawing = true;
- this.placeNode(event);
- }
-
- onMouseMove(event: MouseEvent): void {
- if (this.isDrawing) {
- this.placeNode(event);
- }
- }
-
- getGridPosition(event: MouseEvent): { row: number, col: number } {
- const rect = this.canvas.nativeElement.getBoundingClientRect();
- const x = event.clientX - rect.left;
- const y = event.clientY - rect.top;
-
- const col = Math.floor(x / NODE_SIZE);
- const row = Math.floor(y / NODE_SIZE);
-
- return { row, col };
- }
-
- isValidPosition(row: number, col: number): boolean {
- return row >= 0 && row < GRID_ROWS && col >= 0 && col < GRID_COLS;
- }
-
- onMouseUp(): void {
- this.isDrawing = false;
- this.lastRow = -1;
- this.lastCol = -1;
- }
-
- placeNode(event: MouseEvent): void {
- const rect = this.canvas.nativeElement.getBoundingClientRect();
- const x = event.clientX - rect.left;
- const y = event.clientY - rect.top;
-
- const col = Math.floor(x / NODE_SIZE);
- const row = Math.floor(y / NODE_SIZE);
-
- if (row < 0 || row >= GRID_ROWS || col < 0 || col >= GRID_COLS) {
- return;
- }
-
- if (this.lastRow === row && this.lastCol === col) {
- return;
- }
- this.lastRow = row;
- this.lastCol = col;
-
- const node = this.grid[row][col];
-
- switch (this.selectedNodeType) {
- case NodeType.Start:
- if (!node.isEnd && !node.isWall) {
- if (this.startNode) {
- this.startNode.isStart = false;
- this.drawNode(this.startNode);
- }
- node.isStart = true;
- this.startNode = node;
- }
- break;
-
- case NodeType.End:
- if (!node.isStart && !node.isWall) {
- if (this.endNode) {
- this.endNode.isEnd = false;
- this.drawNode(this.endNode);
- }
- node.isEnd = true;
- this.endNode = node;
- }
- break;
-
- case NodeType.Wall:
- if (!node.isStart && !node.isEnd) {
- if (node.isWall !== this.shouldAddWall) {
- node.isWall = this.shouldAddWall;
- }
- }
- break;
-
- case NodeType.None:
- if (node.isStart) {
- node.isStart = false;
- this.startNode = null;
- } else if (node.isEnd) {
- node.isEnd = false;
- this.endNode = null;
- } else if (node.isWall) {
- node.isWall = false;
- }
- break;
- }
-
- this.drawNode(node);
- }
-
- visualizeDijkstra(): void {
+ edgeCase(): void {
this.stopAnimations();
- if (!this.startNode || !this.endNode) {
- alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
- return;
- }
- this.clearPath();
- const startTime = performance.now();
- const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.dijkstra(this.grid,
- this.grid[this.startNode.row][this.startNode.col],
- this.grid[this.endNode.row][this.endNode.col]
- );
- const endTime = performance.now();
- this.pathLength = nodesInShortestPathOrder.length;
- this.executionTime = endTime - startTime;
- this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
- }
-
- visualizeAStar(): void {
- this.stopAnimations();
- if (!this.startNode || !this.endNode) {
- alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
- return;
- }
- this.clearPath();
- const startTime = performance.now();
- const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.aStar(this.grid,
- this.grid[this.startNode.row][this.startNode.col],
- this.grid[this.endNode.row][this.endNode.col]
- );
- const endTime = performance.now();
- this.pathLength = nodesInShortestPathOrder.length;
- this.executionTime = endTime - startTime;
- this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
- }
-
- animateAlgorithm(visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[]): void {
- for (let i = 0; i <= visitedNodesInOrder.length; i++) {
- if (i === visitedNodesInOrder.length) {
- const timeoutId = setTimeout(() => {
- this.animateShortestPath(nodesInShortestPathOrder);
- }, this.animationSpeed * i);
- this.timeoutIds.push(timeoutId);
- return;
- }
-
- const node = visitedNodesInOrder[i];
- const timeoutId = setTimeout(() => {
- if (!node.isStart && !node.isEnd) {
- node.isVisited = true;
- this.drawNode(node);
- }
- }, this.animationSpeed * i);
- this.timeoutIds.push(timeoutId);
- }
- }
-
- animateShortestPath(nodesInShortestPathOrder: Node[]): void {
- for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
- const node = nodesInShortestPathOrder[i];
- const timeoutId = setTimeout(() => {
- if (!node.isStart && !node.isEnd) {
- node.isPath = true;
- this.drawNode(node);
- }
- }, this.animationSpeed * i);
- this.timeoutIds.push(timeoutId);
- }
- }
-
- drawNode(node: Node): void {
- if (!this.ctx) return;
-
- let color = 'lightgray';
- if (node.isStart) color = 'green';
- else if (node.isEnd) color = 'red';
- else if (node.isPath) color = 'gold';
- else if (node.isVisited) color = 'skyblue';
- else if (node.isWall) color = 'black';
-
- this.ctx.fillStyle = color;
- this.ctx.fillRect(node.col * NODE_SIZE, node.row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
- this.ctx.strokeStyle = '#ccc';
- this.ctx.strokeRect(node.col * NODE_SIZE, node.row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
- }
-
- resetBoard(): void {
- this.stopAnimations();
- this.initializeGrid(true);
+ this.initializeGrid(true, 'edge');
this.drawGrid();
}
clearBoard(): void {
this.stopAnimations();
- this.initializeGrid(false);
+ this.initializeGrid(false, 'edge');
this.drawGrid();
}
- clearPath(): void {
+ visualizeDijkstra(): void {
+ if (!this.ensureStartAndEnd()) {
+ return;
+ }
+
this.stopAnimations();
- for (let row = 0; row < GRID_ROWS; row++) {
- for (let col = 0; col < GRID_COLS; col++) {
+ this.clearPath();
+
+ const startTime = performance.now();
+ const result = this.pathfindingService.dijkstra(
+ this.grid,
+ this.grid[this.startNode!.row][this.startNode!.col],
+ this.grid[this.endNode!.row][this.endNode!.col]
+ );
+ const endTime = performance.now();
+
+ this.pathLength = result.nodesInShortestPathOrder.length;
+ this.executionTime = endTime - startTime;
+
+ this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
+ }
+
+ visualizeAStar(): void {
+ if (!this.ensureStartAndEnd()) {
+ return;
+ }
+
+ this.stopAnimations();
+ this.clearPath();
+
+ const startTime = performance.now();
+ const result = this.pathfindingService.aStar(
+ this.grid,
+ this.grid[this.startNode!.row][this.startNode!.col],
+ this.grid[this.endNode!.row][this.endNode!.col]
+ );
+ const endTime = performance.now();
+
+ this.pathLength = result.nodesInShortestPathOrder.length;
+ this.executionTime = endTime - startTime;
+
+ this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
+ }
+
+ // Mouse interactions
+ private onMouseDown(event: MouseEvent): void {
+ 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: boolean, scenario: 'normal' | 'edge'): void {
+ this.grid = this.createEmptyGrid();
+
+ 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;
+
+ if (withWalls) {
+ this.placeDefaultDiagonalWall();
+ }
+ }
+
+ 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,
+ fScore: 0
+ };
+ }
+
+ private getScenarioStartEnd(scenario: 'normal' | 'edge'): { start: GridPos; end: GridPos } {
+ if (scenario === 'edge') {
+ return {
+ start: { row: 0, col: 0 },
+ end: { row: this.gridRows - 1, col: this.gridCols - 1 }
+ };
+ }
+
+ // 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 placeDefaultDiagonalWall(): void {
+ // 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;
@@ -353,4 +319,211 @@ export class PathfindingComponent implements AfterViewInit {
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)
+ private trySetStart(node: Node): void {
+ if (!this.canBeStart(node)) {
+ return;
+ }
+
+ if (this.startNode) {
+ this.startNode.isStart = false;
+ this.drawNode(this.startNode);
+ }
+
+ node.isStart = true;
+ this.startNode = node;
+ }
+
+ private trySetEnd(node: Node): void {
+ if (!this.canBeEnd(node)) {
+ return;
+ }
+
+ if (this.endNode) {
+ this.endNode.isEnd = false;
+ this.drawNode(this.endNode);
+ }
+
+ node.isEnd = true;
+ this.endNode = node;
+ }
+
+ private tryToggleWall(node: Node, shouldBeWall: boolean): void {
+ if (!this.canBeWall(node)) {
+ return;
+ }
+ node.isWall = shouldBeWall;
+ }
+
+ private tryClearNode(node: Node): void {
+ if (node.isStart) {
+ node.isStart = false;
+ this.startNode = null;
+ return;
+ }
+
+ if (node.isEnd) {
+ node.isEnd = false;
+ this.endNode = null;
+ return;
+ }
+
+ if (node.isWall) {
+ node.isWall = false;
+ }
+ }
+
+ private canBeStart(node: Node): boolean {
+ return !node.isEnd && !node.isWall;
+ }
+
+ private canBeEnd(node: Node): boolean {
+ return !node.isStart && !node.isWall;
+ }
+
+ private canBeWall(node: Node): boolean {
+ return !node.isStart && !node.isEnd;
+ }
+
+ private shouldStartWallStroke(pos: GridPos): boolean {
+ if (this.selectedNodeType !== NodeType.Wall) {
+ return true;
+ }
+
+ const node = this.grid[pos.row][pos.col];
+ return !node.isWall;
+ }
+
+ // Validation
+ private ensureStartAndEnd(): boolean {
+ if (this.startNode && this.endNode) {
+ return true;
+ }
+
+ alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
+ 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): GridPos | null {
+ const rect = this.canvas.nativeElement.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+
+ 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;
+ }
+
+ protected readonly UrlConstants = UrlConstants;
}
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts
index 251af88..f766798 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts
@@ -11,6 +11,11 @@ export interface Node {
fScore: number;
}
-export const GRID_ROWS = 150;
-export const GRID_COLS = 100;
-export const NODE_SIZE = 10; // in pixels
+export const DEFAULT_GRID_ROWS = 50;
+export const DEFAULT_GRID_COLS = 50;
+
+export const MIN_GRID_SIZE = 2;
+export const MAX_GRID_SIZE = 150;
+
+// Canvas max size (px)
+export const MAX_GRID_PX = 1000;
diff --git a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts
index abebff3..e6cc349 100644
--- a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts
+++ b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts
@@ -1,22 +1,20 @@
import { Injectable } from '@angular/core';
-import { Node, GRID_ROWS, GRID_COLS } from '../pathfinding.models';
+import { Node} from '../pathfinding.models';
@Injectable({
providedIn: 'root'
})
export class PathfindingService {
-
-
// Helper function to get all unvisited neighbors of a given node
getUnvisitedNeighbors(node: Node, grid: Node[][]): Node[] {
const neighbors: Node[] = [];
const { col, row } = node;
if (row > 0) neighbors.push(grid[row - 1][col]);
- if (row < GRID_ROWS - 1) neighbors.push(grid[row + 1][col]);
+ if (row < grid.length - 1) neighbors.push(grid[row + 1][col]);
if (col > 0) neighbors.push(grid[row][col - 1]);
- if (col < GRID_COLS - 1) neighbors.push(grid[row][col + 1]);
+ if (col < grid[0].length - 1) neighbors.push(grid[row][col + 1]);
return neighbors.filter(neighbor => !neighbor.isVisited && !neighbor.isWall);
}
@@ -42,16 +40,23 @@ export class PathfindingService {
this.sortNodesByDistance(unvisitedNodes);
const closestNode = unvisitedNodes.shift() as Node;
- // If we encounter a wall, skip it
- if (closestNode.isWall) continue;
+ if (closestNode.isWall) {
+ continue;
+ }
- // If the closest node is at an infinite distance, we're trapped
- if (closestNode.distance === Infinity) return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
+ const isTrapped = closestNode.distance === Infinity;
+ if (isTrapped)
+ {
+ return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
+ }
closestNode.isVisited = true;
visitedNodesInOrder.push(closestNode);
- if (closestNode === endNode) return { visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode) };
+ const reachedTheEnd = closestNode === endNode;
+ if (reachedTheEnd) {
+ return {visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode)};
+ }
this.updateUnvisitedNeighbors(closestNode, grid);
}
@@ -75,7 +80,6 @@ export class PathfindingService {
aStar(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
const visitedNodesInOrder: Node[] = [];
startNode.distance = 0;
- // hueristic distance
startNode['distance'] = this.calculateHeuristic(startNode, endNode);
// fScore = gScore + hScore
startNode['fScore'] = startNode.distance + startNode['distance'];
@@ -83,29 +87,28 @@ export class PathfindingService {
const openSet: Node[] = [startNode];
const allNodes = this.getAllNodes(grid);
- // Initialize all nodes' fScore to infinity except for the startNode
- for (const node of allNodes) {
- if (node !== startNode) {
- node['fScore'] = Infinity;
- node.distance = Infinity; // gScore
- }
- }
-
+ this.initNodesForAStar(allNodes, startNode);
while (openSet.length > 0) {
openSet.sort((nodeA, nodeB) => nodeA['fScore'] - nodeB['fScore']);
const currentNode = openSet.shift() as Node;
- if (currentNode.isWall) continue;
+ if (currentNode.isWall) {
+ continue;
+ }
- // If the closest node is at an infinite distance, we're trapped
- if (currentNode.distance === Infinity) return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
+ const isTrapped = currentNode.distance === Infinity;
+ if (isTrapped)
+ {
+ return {visitedNodesInOrder, nodesInShortestPathOrder: []};
+ }
currentNode.isVisited = true;
visitedNodesInOrder.push(currentNode);
- if (currentNode === endNode) {
+ const reachedTheEnd = currentNode === endNode;
+ if (reachedTheEnd) {
return { visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode) };
}
@@ -114,14 +117,7 @@ export class PathfindingService {
const tentativeGScore = currentNode.distance + 1; // Distance from start to neighbor
if (tentativeGScore < neighbor.distance) {
- neighbor.previousNode = currentNode;
- neighbor.distance = tentativeGScore;
- neighbor['distance'] = this.calculateHeuristic(neighbor, endNode);
- neighbor['fScore'] = neighbor.distance + neighbor['distance'];
-
- if (!openSet.includes(neighbor)) {
- openSet.push(neighbor);
- }
+ this.updateNeighborNode(neighbor, currentNode, tentativeGScore, endNode, openSet);
}
}
}
@@ -129,6 +125,26 @@ export class PathfindingService {
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
}
+ private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) {
+ neighbor.previousNode = currentNode;
+ neighbor.distance = tentativeGScore;
+ neighbor['distance'] = this.calculateHeuristic(neighbor, endNode);
+ neighbor['fScore'] = neighbor.distance + neighbor['distance'];
+
+ if (!openSet.includes(neighbor)) {
+ openSet.push(neighbor);
+ }
+ }
+
+ private initNodesForAStar(allNodes: Node[], startNode: Node) {
+ for (const node of allNodes) {
+ if (node !== startNode) {
+ node['fScore'] = Infinity;
+ node.distance = Infinity; // gScore
+ }
+ }
+ }
+
private calculateHeuristic(node: Node, endNode: Node): number {
// Manhattan distance heuristic
return Math.abs(node.row - endNode.row) + Math.abs(node.col - endNode.col);
diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/service/algorithms.service.ts
index c84b802..a317759 100644
--- a/src/app/pages/algorithms/service/algorithms.service.ts
+++ b/src/app/pages/algorithms/service/algorithms.service.ts
@@ -1,25 +1,20 @@
import { Injectable } from '@angular/core';
import { AlgorithmCategory } from '../models/algorithm-category';
import { Observable, of } from 'rxjs';
+import {RouterConstants} from '../../../constants/RouterConstants';
@Injectable({
providedIn: 'root'
})
export class AlgorithmsService {
- private categories: AlgorithmCategory[] = [
+ private readonly categories: AlgorithmCategory[] = [
{
id: 'pathfinding',
- title: 'Pfadfindungsalgorithmen',
- description: 'Vergleich von Pfadfindungsalgorithmen wie Dijkstra und A*.',
- routerLink: 'pathfinding'
- },
- // {
- // id: 'sorting',
- // title: 'Sortieralgorithmen',
- // description: 'Visualisierung von Sortieralgorithmen wie Bubble Sort, Merge Sort und Quick Sort.',
- // routerLink: 'sorting'
- // }
+ title: 'ALGORITHM.PATHFINDING.TITLE',
+ description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
+ routerLink: RouterConstants.PATHFINDING.LINK
+ }
];
getCategories(): Observable {
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index 02139e0..77b96d7 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -301,14 +301,29 @@
"CLEAR_NODE": "Löschen",
"DIJKSTRA": "Dijkstra",
"ASTAR": "A*",
- "RESET_BOARD": "Board zurücksetzten",
+ "NORMAL_CASE": "Testaufbau",
+ "EDGE_CASE": "A* Grenzfall",
"CLEAR_BOARD": "Board leeren",
"VISITED": "Besucht",
"PATH": "Pfad",
"PATH_LENGTH": "Pfadlänge",
"EXECUTION_TIME": "Ausführungszeit",
+ "EXPLANATION": {
+ "TITLE": "Algorithmen",
+ "DIJKSTRA_EXPLANATION": " findet garantiert den kürzesten Weg, wenn alle Kantenkosten nicht-negativ sind. Vorteil: optimal und ohne Heuristik. Nachteil: besucht oft sehr viele Knoten (kann bei großen Grids langsamer wirken).",
+ "ASTAR_EXPLANATION": " erweitert Dijkstra um eine Heuristik (z.B. Manhattan-Distanz) und kann dadurch wesentlich zielgerichteter suchen. Vorteil: oft deutlich schneller bei guter Heuristik; bei zulässiger Heuristik bleibt der Weg optimal. Nachteil: hängt stark von der Heuristik ab (schlechte Heuristik ≈ Dijkstra)."
+ },
"ALERT": {
"START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten."
}
+ },
+ "ALGORITHM": {
+ "TITLE": "Algorithmen",
+ "PATHFINDING": {
+ "TITLE": "Wegfindung",
+ "DESCRIPTION": "Vergleich von Dijkstra vs. A*.",
+ "GRID_HEIGHT": "Höhe",
+ "GRID_WIDTH": "Beite"
+ }
}
}
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index c5a8996..4bb9c6e 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -301,14 +301,29 @@
"CLEAR_NODE": "Clear",
"DIJKSTRA": "Dijkstra",
"ASTAR": "A*",
- "RESET_BOARD": "Reset Board",
+ "NORMAL_CASE": "Test Scenario",
+ "EDGE_CASE": "A* Edge Case",
"CLEAR_BOARD": "Clear Board",
"VISITED": "Visited",
"PATH": "Path",
"PATH_LENGTH": "Path length",
"EXECUTION_TIME": "Execution Time",
+ "EXPLANATION": {
+ "TITLE": "Algorithms",
+ "DIJKSTRA_EXPLANATION": " is guaranteed to find the shortest path if all edge costs are non-negative. Advantage: optimal and without heuristics. Disadvantage: often visits a large number of nodes (can be slower for large grids).",
+ "ASTAR_EXPLANATION": " extends Dijkstra with a heuristic (e.g. Manhattan distance) and can therefore search in a much more targeted manner. Advantage: often significantly faster with good heuristics; with permissible heuristics, the path remains optimal. Disadvantage: highly dependent on heuristics (poor heuristics ≈ Dijkstra)."
+ },
"ALERT": {
"START_END_NODES": "Please select a start and end node before running the algorithm."
}
+ },
+ "ALGORITHM": {
+ "TITLE": "Algorithms",
+ "PATHFINDING": {
+ "TITLE": "Pathfinding",
+ "DESCRIPTION": "Comparing of Dijkstra vs. A*.",
+ "GRID_HEIGHT": "Height",
+ "GRID_WIDTH": "Width"
+ }
}
}
diff --git a/src/main.ts b/src/main.ts
index f28c38d..b1a88d7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,8 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import packageJson from '../package.json';
import {AppComponent} from './app/layout/app/app.component';
-import { register } from 'swiper/element/bundle';
-register();
if (packageJson.version) {
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 264f459..8b5e62c 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -3,6 +3,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
+ "resolveJsonModule": true,
"outDir": "./out-tsc/app",
"types": []
},
diff --git a/tsconfig.json b/tsconfig.json
index e4955f2..bbdee77 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,10 @@
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
- "module": "preserve"
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "esModuleInterop": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,