Merge pull request 'feature/pathfinding-finetuning' (#11) from feature/pathfinding-finetuning into main
All checks were successful
Build & Push Frontend A / docker (push) Successful in 50s
All checks were successful
Build & Push Frontend A / docker (push) Successful in 50s
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
@@ -66,7 +66,7 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-card class="experdience">
|
<mat-card class="experdience">
|
||||||
<h2>{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
|
<h2 style="margin-left: 0.5rem;">{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
|
||||||
<div class="xp-list">
|
<div class="xp-list">
|
||||||
@for (entry of xpKeys; track entry.key) {
|
@for (entry of xpKeys; track entry.key) {
|
||||||
<div class="xp-item">
|
<div class="xp-item">
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
/* Skills block */
|
/* Skills block */
|
||||||
.skills {
|
.skills {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
h2 { margin-top: .25rem; margin-left: .25rem; }
|
h2 { margin-top: .25rem; margin-left: .25rem; }
|
||||||
.chip-groups {
|
.chip-groups {
|
||||||
margin-left: .25rem;
|
margin-left: .25rem;
|
||||||
@@ -64,11 +63,7 @@
|
|||||||
/* Experience block */
|
/* Experience block */
|
||||||
.experience {
|
.experience {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
h2 { margin-top: .25rem;margin-left: .25rem; }
|
h2 { margin-top: .25rem; margin-left: .25rem; }
|
||||||
.xp-list {
|
|
||||||
margin-left: .25rem;
|
|
||||||
display: grid; gap: .75rem;
|
|
||||||
}
|
|
||||||
.xp-item {
|
.xp-item {
|
||||||
.xp-head {
|
.xp-head {
|
||||||
display:flex; align-items:baseline; gap:.5rem;
|
display:flex; align-items:baseline; gap:.5rem;
|
||||||
|
|||||||
@@ -23,13 +23,14 @@
|
|||||||
|
|
||||||
<div class="controls-container">
|
<div class="controls-container">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button matButton="filled" (click)="visualizeDijkstra()">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||||
<button matButton="filled" (click)="visualizeAStar()">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button matButton="filled" (click)="normalCase()">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
|
||||||
<button matButton="filled" (click)="edgeCase()">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'random'})">{{ 'PATHFINDING.RANDOM_CASE' | translate }}</button>
|
||||||
<button matButton="filled" (click)="clearBoard()">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
|
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'edge'})">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button>
|
||||||
|
<button matButton="filled" (click)="createCase({withWalls: false, scenario: 'normal'})">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {MatInputModule} from '@angular/material/input';
|
|||||||
|
|
||||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
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 {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MAX_RANDOM_WALLS_FACTORS, MIN_GRID_SIZE, Node} from './pathfinding.models';
|
||||||
import {PathfindingService} from './service/pathfinding.service';
|
import {PathfindingService} from './service/pathfinding.service';
|
||||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
@@ -70,7 +70,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
private shouldAddWall = true;
|
private shouldAddWall = true;
|
||||||
|
|
||||||
animationSpeed = 3;
|
animationSpeed = 3;
|
||||||
pathLength = 0;
|
pathLength = "0";
|
||||||
executionTime = 0;
|
executionTime = 0;
|
||||||
|
|
||||||
private timeoutIds: number[] = [];
|
private timeoutIds: number[] = [];
|
||||||
@@ -103,40 +103,32 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
applyGridSize(skipReset?: boolean): void {
|
applyGridSize(skipReset?: boolean): void {
|
||||||
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
|
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
|
||||||
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
|
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
|
||||||
|
|
||||||
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
|
|
||||||
if (skipReset) {
|
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
|
||||||
this.initializeGrid(true, 'edge');
|
{
|
||||||
this.drawGrid();
|
this.drawGrid();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default after size changes: pick one consistent scenario
|
if (skipReset) {
|
||||||
this.edgeCase();
|
this.initializeGrid({withWalls: true, scenario: 'normal'});
|
||||||
|
this.drawGrid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createCase({withWalls: true, scenario: 'normal'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scenarios (buttons)
|
createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void
|
||||||
normalCase(): void {
|
{
|
||||||
this.stopAnimations();
|
this.stopAnimations();
|
||||||
this.initializeGrid(true, 'normal');
|
this.initializeGrid({withWalls, scenario});
|
||||||
this.drawGrid();
|
this.drawGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeCase(): void {
|
visualize(algorithm: string): void {
|
||||||
this.stopAnimations();
|
|
||||||
this.initializeGrid(true, 'edge');
|
|
||||||
this.drawGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearBoard(): void {
|
|
||||||
this.stopAnimations();
|
|
||||||
this.initializeGrid(false, 'edge');
|
|
||||||
this.drawGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
visualizeDijkstra(): void {
|
|
||||||
if (!this.ensureStartAndEnd()) {
|
if (!this.ensureStartAndEnd()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -145,36 +137,38 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
this.clearPath();
|
this.clearPath();
|
||||||
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const result = this.pathfindingService.dijkstra(
|
let result;
|
||||||
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;
|
switch (algorithm) {
|
||||||
this.executionTime = endTime - startTime;
|
case 'dijkstra': result = this.pathfindingService.dijkstra(
|
||||||
|
this.grid,
|
||||||
|
this.grid[this.startNode!.row][this.startNode!.col],
|
||||||
|
this.grid[this.endNode!.row][this.endNode!.col]
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
if (!result)
|
||||||
}
|
{
|
||||||
|
|
||||||
visualizeAStar(): void {
|
|
||||||
if (!this.ensureStartAndEnd()) {
|
|
||||||
return;
|
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();
|
const endTime = performance.now();
|
||||||
|
const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
|
||||||
this.pathLength = result.nodesInShortestPathOrder.length;
|
if (lengthOfShortestPath === 0)
|
||||||
|
{
|
||||||
|
this.pathLength = "∞"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.pathLength = result.nodesInShortestPathOrder.length + "";
|
||||||
|
}
|
||||||
this.executionTime = endTime - startTime;
|
this.executionTime = endTime - startTime;
|
||||||
|
|
||||||
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
||||||
@@ -244,7 +238,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grid init
|
// Grid init
|
||||||
private initializeGrid(withWalls: boolean, scenario: 'normal' | 'edge'): void {
|
private initializeGrid({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
|
||||||
this.grid = this.createEmptyGrid();
|
this.grid = this.createEmptyGrid();
|
||||||
|
|
||||||
const { start, end } = this.getScenarioStartEnd(scenario);
|
const { start, end } = this.getScenarioStartEnd(scenario);
|
||||||
@@ -255,7 +249,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
this.endNode.isEnd = true;
|
this.endNode.isEnd = true;
|
||||||
|
|
||||||
if (withWalls) {
|
if (withWalls) {
|
||||||
this.placeDefaultDiagonalWall();
|
this.placeDefaultDiagonalWall(scenario);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,23 +283,105 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getScenarioStartEnd(scenario: 'normal' | 'edge'): { start: GridPos; end: GridPos } {
|
private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
|
||||||
if (scenario === 'edge') {
|
if (scenario === 'edge') {
|
||||||
return {
|
return {
|
||||||
start: { row: 0, col: 0 },
|
start: { row: 0, col: 0 },
|
||||||
end: { row: this.gridRows - 1, col: this.gridCols - 1 }
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// normal: mid-left -> mid-right
|
|
||||||
const midRow = Math.floor(this.gridRows / 2);
|
|
||||||
return {
|
return {
|
||||||
start: { row: midRow, col: 0 },
|
start: {row: startRow, col: startCol},
|
||||||
end: { row: midRow, col: this.gridCols - 1 }
|
end: {row: endRow, col: endCol}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private placeDefaultDiagonalWall(): void {
|
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
|
// Diagonal-ish wall; avoids start/end
|
||||||
const len = Math.min(this.gridRows, this.gridCols);
|
const len = Math.min(this.gridRows, this.gridCols);
|
||||||
const startCol = Math.floor((this.gridCols - len) / 2);
|
const startCol = Math.floor((this.gridCols - len) / 2);
|
||||||
@@ -327,7 +403,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path state
|
// Path state
|
||||||
private clearPath(): void {
|
private clearPath(): void {
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
@@ -563,5 +639,9 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private randomIntFromInterval(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly UrlConstants = UrlConstants;
|
protected readonly UrlConstants = UrlConstants;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ export const MAX_GRID_SIZE = 150;
|
|||||||
|
|
||||||
// Canvas max size (px)
|
// Canvas max size (px)
|
||||||
export const MAX_GRID_PX = 1000;
|
export const MAX_GRID_PX = 1000;
|
||||||
|
export const MAX_RANDOM_WALLS_FACTORS = 0.3;
|
||||||
|
|||||||
@@ -303,7 +303,8 @@
|
|||||||
"ASTAR": "Start A*",
|
"ASTAR": "Start A*",
|
||||||
"NORMAL_CASE": "Testaufbau",
|
"NORMAL_CASE": "Testaufbau",
|
||||||
"EDGE_CASE": "A* Grenzfall-Aufbau",
|
"EDGE_CASE": "A* Grenzfall-Aufbau",
|
||||||
"CLEAR_BOARD": "Board leeren",
|
"RANDOM_CASE": "Zufälliger-Aufbau",
|
||||||
|
"CLEAR_BOARD": "Leeres Gitter",
|
||||||
"VISITED": "Besucht",
|
"VISITED": "Besucht",
|
||||||
"PATH": "Pfad",
|
"PATH": "Pfad",
|
||||||
"PATH_LENGTH": "Pfadlänge",
|
"PATH_LENGTH": "Pfadlänge",
|
||||||
|
|||||||
@@ -303,7 +303,8 @@
|
|||||||
"ASTAR": "Start A*",
|
"ASTAR": "Start A*",
|
||||||
"NORMAL_CASE": "Test Scenario",
|
"NORMAL_CASE": "Test Scenario",
|
||||||
"EDGE_CASE": "A* Edge Case Scenario",
|
"EDGE_CASE": "A* Edge Case Scenario",
|
||||||
"CLEAR_BOARD": "Clear Board",
|
"RANDOM_CASE": "Random Case",
|
||||||
|
"CLEAR_BOARD": "Empty Board",
|
||||||
"VISITED": "Visited",
|
"VISITED": "Visited",
|
||||||
"PATH": "Path",
|
"PATH": "Path",
|
||||||
"PATH_LENGTH": "Path length",
|
"PATH_LENGTH": "Path length",
|
||||||
|
|||||||
Reference in New Issue
Block a user