From e0f0a0ed0493bd3121c4104f91eae614f8881037 Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Mon, 2 Feb 2026 10:06:59 +0100 Subject: [PATCH] Enhance pathfinding UI with grid resizing and scenarios Added controls for dynamic grid size adjustment and scenario presets (normal and edge case) to the pathfinding component. Improved UI/UX with algorithm explanations, Wikipedia links, and reorganized controls. Refactored grid logic for flexibility, updated translations, and improved code structure for maintainability. --- src/app/constants/UrlConstants.ts | 2 + .../pathfinding/pathfinding.component.html | 58 +- .../pathfinding/pathfinding.component.scss | 34 +- .../pathfinding/pathfinding.component.ts | 773 +++++++++++------- .../pathfinding/pathfinding.models.ts | 11 +- .../service/pathfinding.service.ts | 6 +- src/assets/i18n/de.json | 12 +- src/assets/i18n/en.json | 12 +- 8 files changed, 591 insertions(+), 317 deletions(-) diff --git a/src/app/constants/UrlConstants.ts b/src/app/constants/UrlConstants.ts index ec5fe29..a730a49 100644 --- a/src/app/constants/UrlConstants.ts +++ b/src/app/constants/UrlConstants.ts @@ -1,4 +1,6 @@ export class UrlConstants { static readonly LINKED_IN = 'https://www.linkedin.com/in/andreas-dahm-2395991ba'; static readonly GIT_HUB = 'https://github.com/LoboTheDark'; + static readonly DIJKSTRA_WIKI = 'https://de.wikipedia.org/wiki/Dijkstra-Algorithmus' + static readonly ASTAR_WIKI = 'https://de.wikipedia.org/wiki/A*-Algorithmus' } diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 0e391e7..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 +

+
+
@@ -10,11 +24,43 @@ {{ 'PATHFINDING.CLEAR_NODE' | translate }}
+
- - - - + + + + + +
+ +
+
+ + {{ 'ALGORITHM.PATHFINDING.GRID_HEIGHT' | translate }} + + + + + {{ 'ALGORITHM.PATHFINDING.GRID_WIDTH' | translate }} + + +
@@ -26,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 bc30072..e6cc349 100644 --- a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts +++ b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Node, GRID_ROWS, GRID_COLS } from '../pathfinding.models'; +import { Node} from '../pathfinding.models'; @Injectable({ providedIn: 'root' @@ -12,9 +12,9 @@ export class PathfindingService { 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); } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 0c92e21..77b96d7 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -301,12 +301,18 @@ "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." } @@ -315,7 +321,9 @@ "TITLE": "Algorithmen", "PATHFINDING": { "TITLE": "Wegfindung", - "DESCRIPTION": "Vergleich von Dijkstra vs. A*." + "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 2999621..4bb9c6e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -301,12 +301,18 @@ "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." } @@ -315,7 +321,9 @@ "TITLE": "Algorithms", "PATHFINDING": { "TITLE": "Pathfinding", - "DESCRIPTION": "Comparing of Dijkstra vs. A*." + "DESCRIPTION": "Comparing of Dijkstra vs. A*.", + "GRID_HEIGHT": "Height", + "GRID_WIDTH": "Width" } } }