Merge pull request 'feature/algorithm_pathfinding' (#3) from feature/algorithm_pathfinding into main
All checks were successful
Build & Push Frontend A / docker (push) Successful in 1m28s

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-02-01 17:04:35 +01:00
27 changed files with 2777 additions and 67 deletions

View File

@@ -3,7 +3,10 @@
"version": 1,
"cli": {
"packageManager": "npm",
"analytics": false
"analytics": false,
"schematicCollections": [
"angular-eslint"
]
},
"newProjectRoot": "projects",
"projects": {
@@ -95,6 +98,15 @@
"src/styles.scss"
]
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}

44
eslint.config.js Normal file
View File

@@ -0,0 +1,44 @@
// @ts-check
const eslint = require("@eslint/js");
const { defineConfig } = require("eslint/config");
const tseslint = require("typescript-eslint");
const angular = require("angular-eslint");
module.exports = defineConfig([
{
files: ["**/*.ts"],
extends: [
eslint.configs.recommended,
tseslint.configs.recommended,
tseslint.configs.stylistic,
angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
"@angular-eslint/directive-selector": [
"error",
{
type: "attribute",
prefix: "app",
style: "camelCase",
},
],
"@angular-eslint/component-selector": [
"error",
{
type: "element",
prefix: "app",
style: "kebab-case",
},
],
},
},
{
files: ["**/*.html"],
extends: [
angular.configs.templateRecommended,
angular.configs.templateAccessibility,
],
rules: {},
}
]);

1951
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,8 @@
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"lint": "ng lint"
},
"private": true,
"dependencies": {
@@ -35,7 +36,10 @@
"@angular/compiler-cli": "~21.1.0",
"@angular/platform-browser-dynamic": "~21.1.0",
"@types/jasmine": "~5.1.15",
"angular-eslint": "21.2.0",
"eslint": "^9.39.2",
"jasmine-core": "~6.0.1",
"typescript": "~5.9.3"
"typescript": "~5.9.3",
"typescript-eslint": "8.50.1"
}
}
}

View File

@@ -1,14 +1,16 @@
import { Routes } from '@angular/router';
import {AboutComponent} from './pages/about/about.component';
import {ProjectsComponent} from './pages/projects/projects.component';
import {HobbiesComponent} from './pages/hobbies/hobbies.component';
import {ImprintComponent} from './pages/imprint/imprint.component';
import {AlgorithmsComponent} from './pages/algorithms/algorithms.component';
import {PathfindingComponent} from './pages/algorithms/pathfinding/pathfinding.component';
export const routes: Routes = [
{ path: '', component: AboutComponent },
{ path: 'about', component: AboutComponent},
{ path: 'projects', component: ProjectsComponent},
{ path: 'hobbies', component: HobbiesComponent},
{ path: 'algorithms', component: AlgorithmsComponent},
{ path: 'algorithms/pathfinding', component: PathfindingComponent },
{ path: 'imprint', component: ImprintComponent},
];

View File

@@ -1,11 +1,9 @@
import {
Container,
MoveDirection,
OutMode,
Engine
} from "@tsparticles/engine";
import {NgParticlesService, NgxParticlesModule} from "@tsparticles/angular";
import {Component} from '@angular/core';
import { Component, inject } from '@angular/core';
import {loadFull} from 'tsparticles';
@Component({
@@ -18,6 +16,8 @@ import {loadFull} from 'tsparticles';
styleUrl: './particles-bg.component.scss',
})
export class ParticlesBgComponent {
private readonly ngParticlesService = inject(NgParticlesService);
id = "tsparticles";
/* Starting from 1.19.0 you can use a remote url (AJAX request) to a JSON with the configuration */
@@ -92,7 +92,7 @@ export class ParticlesBgComponent {
detectRetina: true,*/
};
constructor(private readonly ngParticlesService: NgParticlesService) {}
async particlesInit(engine: Engine): Promise<void> {
await loadFull(engine);
}

View File

@@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@@ -63,7 +63,10 @@ import { MatIconModule } from '@angular/material/icon';
`],
})
export class ImageDialogComponent {
constructor(@Inject(MAT_DIALOG_DATA) public data: { title: string; src: string }) {
console.log(data.title);
}
data = inject<{
title: string;
src: string;
}>(MAT_DIALOG_DATA);
}

View File

@@ -12,7 +12,7 @@
<nav class="nav">
<a routerLink="/about" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
<a routerLink="/projects" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
<a routerLink="/hobbies" mat-button>{{ 'TOPBAR.HOBBY' | translate }}</a>
<a routerLink="/algorithms" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
<a routerLink="/imprint" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
</nav>
@@ -35,8 +35,8 @@
<button mat-menu-item routerLink="/projects">
{{ 'TOPBAR.PROJECTS' | translate }}
</button>
<button mat-menu-item routerLink="/hobbys">
{{ 'TOPBAR.HOBBY' | translate }}
<button mat-menu-item routerLink="/algorithms">
{{ 'TOPBAR.ALGORITHMS' | translate }}
</button>
<button mat-menu-item routerLink="/imprint">
{{ 'TOPBAR.IMPRINT' | translate }}

View File

@@ -65,7 +65,7 @@
</div>
</mat-card>
<mat-card class="experience">
<mat-card class="experdience">
<h2>{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
<div class="xp-list">
@for (entry of xpKeys; track entry.key) {

View File

@@ -0,0 +1,15 @@
<div class="container">
<h1>Algorithmen</h1>
<div class="category-cards">
@for (category of categories$ | async; track category.id) {
<mat-card [routerLink]="[category.routerLink]">
<mat-card-header>
<mat-card-title>{{ category.title }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>{{ category.description }}</p>
</mat-card-content>
</mat-card>
}
</div>
</div>

View File

@@ -0,0 +1,19 @@
.container {
padding: 2rem;
}
.category-cards {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
mat-card {
cursor: pointer;
max-width: 300px;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}

View File

@@ -0,0 +1,27 @@
import { Component, OnInit, inject } from '@angular/core';
import { AlgorithmsService } from './service/algorithms.service';
import { AlgorithmCategory } from './models/algorithm-category';
import { Observable } from 'rxjs';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-algorithms',
templateUrl: './algorithms.component.html',
styleUrls: ['./algorithms.component.scss'],
standalone: true,
imports: [CommonModule, RouterLink, MatCardModule],
})
export class AlgorithmsComponent implements OnInit {
private algorithmsService = inject(AlgorithmsService);
categories$: Observable<AlgorithmCategory[]> | undefined;
ngOnInit(): void {
this.categories$ = this.algorithmsService.getCategories();
}
}

View File

@@ -0,0 +1,6 @@
export interface AlgorithmCategory {
id: string;
title: string;
description: string;
routerLink: string;
}

View File

@@ -0,0 +1,35 @@
<div class="container">
<h1>{{ 'PATHFINDING.TITLE' | translate }}</h1>
<div class="controls-container">
<div class="controls">
<mat-button-toggle-group [(ngModel)]="selectedNodeType" aria-label="Node Type Selection">
<mat-button-toggle [value]="NodeType.Start">{{ 'PATHFINDING.START_NODE' | translate }}</mat-button-toggle>
<mat-button-toggle [value]="NodeType.End">{{ 'PATHFINDING.END_NODE' | translate }}</mat-button-toggle>
<mat-button-toggle [value]="NodeType.Wall">{{ 'PATHFINDING.WALL' | translate }}</mat-button-toggle>
<mat-button-toggle [value]="NodeType.None">{{ 'PATHFINDING.CLEAR_NODE' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
<button mat-raised-button color="primary" (click)="visualizeDijkstra()">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
<button mat-raised-button color="accent" (click)="visualizeAStar()">{{ 'PATHFINDING.ASTAR' | translate }}</button>
<button mat-raised-button color="warn" (click)="resetBoard()">{{ 'PATHFINDING.RESET_BOARD' | translate }}</button>
<button mat-raised-button color="warn" (click)="clearBoard()">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
<button mat-raised-button color="info" (click)="clearPath()">{{ 'PATHFINDING.CLEAR_PATH' | translate }}</button>
</div>
<div class="legend">
<span><span class="legend-color start"></span> {{ 'PATHFINDING.START_NODE' | translate }}</span>
<span><span class="legend-color end"></span> {{ 'PATHFINDING.END_NODE' | translate }}</span>
<span><span class="legend-color wall"></span> {{ 'PATHFINDING.WALL' | translate }}</span>
<span><span class="legend-color visited"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
<span><span class="legend-color path"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
</div>
</div>
<canvas #gridCanvas></canvas>
<div class="results-container">
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
</div>
</div>

View File

@@ -0,0 +1,49 @@
.container {
padding: 2rem;
}
.controls-container {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
mat-button-toggle-group {
border-radius: 4px;
overflow: hidden;
}
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: 0.9em;
.legend-color {
display: inline-block;
width: 15px;
height: 15px;
border: 1px solid #ccc;
vertical-align: middle;
margin-right: 5px;
&.start { background-color: green; }
&.end { background-color: red; }
&.wall { background-color: black; }
&.visited { background-color: skyblue; }
&.path { background-color: gold; }
}
}
canvas {
border: 1px solid #ccc;
display: block;
}

View File

@@ -0,0 +1,356 @@
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 {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
enum NodeType {
Start = 'start',
End = 'end',
Wall = 'wall',
None = 'none'
}
@Component({
selector: 'app-pathfinding',
standalone: true,
imports: [CommonModule, MatButtonModule, MatButtonToggleModule, FormsModule, 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[] = [];
@ViewChild('gridCanvas', { static: true })
canvas!: ElementRef<HTMLCanvasElement>;
ctx!: CanvasRenderingContext2D;
grid: Node[][] = [];
startNode: Node | null = null;
endNode: Node | null = null;
isDrawing = false;
shouldAddWall = true;
selectedNodeType: NodeType = NodeType.None; // Default to no selection
animationSpeed = 3; // milliseconds
pathLength = 0;
executionTime = 0;
readonly NodeType = NodeType;
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.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 {
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.drawGrid();
}
clearBoard(): void {
this.stopAnimations();
this.initializeGrid(false);
this.drawGrid();
}
clearPath(): void {
this.stopAnimations();
for (let row = 0; row < GRID_ROWS; row++) {
for (let col = 0; col < GRID_COLS; col++) {
const node = this.grid[row][col];
node.isVisited = false;
node.isPath = false;
node.distance = Infinity;
node.previousNode = null;
}
}
this.drawGrid();
}
}

View File

@@ -0,0 +1,16 @@
export interface Node {
row: number;
col: number;
isStart: boolean;
isEnd: boolean;
isWall: boolean;
isVisited: boolean;
isPath: boolean;
distance: number;
previousNode: Node | null;
fScore: number;
}
export const GRID_ROWS = 150;
export const GRID_COLS = 100;
export const NODE_SIZE = 10; // in pixels

View File

@@ -0,0 +1,146 @@
import { Injectable } from '@angular/core';
import { Node, GRID_ROWS, GRID_COLS } 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 (col > 0) neighbors.push(grid[row][col - 1]);
if (col < GRID_COLS - 1) neighbors.push(grid[row][col + 1]);
return neighbors.filter(neighbor => !neighbor.isVisited && !neighbor.isWall);
}
// Helper function to get the nodes in the shortest path
getNodesInShortestPath(endNode: Node): Node[] {
const shortestPathNodes: Node[] = [];
let currentNode: Node | null = endNode;
while (currentNode !== null) {
shortestPathNodes.unshift(currentNode);
currentNode = currentNode.previousNode;
}
return shortestPathNodes;
}
// Dijkstra's Algorithm
dijkstra(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
const visitedNodesInOrder: Node[] = [];
startNode.distance = 0;
const unvisitedNodes: Node[] = this.getAllNodes(grid);
while (unvisitedNodes.length > 0) {
this.sortNodesByDistance(unvisitedNodes);
const closestNode = unvisitedNodes.shift() as Node;
// If we encounter a wall, skip it
if (closestNode.isWall) continue;
// If the closest node is at an infinite distance, we're trapped
if (closestNode.distance === Infinity) return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
closestNode.isVisited = true;
visitedNodesInOrder.push(closestNode);
if (closestNode === endNode) return { visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode) };
this.updateUnvisitedNeighbors(closestNode, grid);
}
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
}
private sortNodesByDistance(unvisitedNodes: Node[]): void {
unvisitedNodes.sort((nodeA, nodeB) => nodeA.distance - nodeB.distance);
}
private updateUnvisitedNeighbors(node: Node, grid: Node[][]): void {
const unvisitedNeighbors = this.getUnvisitedNeighbors(node, grid);
for (const neighbor of unvisitedNeighbors) {
neighbor.distance = node.distance + 1;
neighbor.previousNode = node;
}
}
// A* Search Algorithm
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'];
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
}
}
while (openSet.length > 0) {
openSet.sort((nodeA, nodeB) => nodeA['fScore'] - nodeB['fScore']);
const currentNode = openSet.shift() as Node;
if (currentNode.isWall) continue;
// If the closest node is at an infinite distance, we're trapped
if (currentNode.distance === Infinity) return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
currentNode.isVisited = true;
visitedNodesInOrder.push(currentNode);
if (currentNode === endNode) {
return { visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode) };
}
const neighbors = this.getUnvisitedNeighbors(currentNode, grid);
for (const neighbor of neighbors) {
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);
}
}
}
}
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
}
private calculateHeuristic(node: Node, endNode: Node): number {
// Manhattan distance heuristic
return Math.abs(node.row - endNode.row) + Math.abs(node.col - endNode.col);
}
private getAllNodes(grid: Node[][]): Node[] {
const nodes: Node[] = [];
for (const row of grid) {
for (const node of row) {
nodes.push(node);
}
}
return nodes;
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { AlgorithmCategory } from '../models/algorithm-category';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AlgorithmsService {
private 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'
// }
];
getCategories(): Observable<AlgorithmCategory[]> {
return of(this.categories);
}
}

View File

@@ -1,3 +0,0 @@
<div class="terminal-loader">
<span>&gt; Work in progress</span><span class="cursor"></span>
</div>

View File

@@ -1,19 +0,0 @@
.terminal-loader {
font-family: monospace;
font-size: 1.1rem;
display: inline-flex;
align-items: center;
}
.cursor {
width: 10px;
height: 1.1rem;
background: var(--app-fg);
margin-left: .25rem;
animation: blink .8s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}

View File

@@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-hobbies',
imports: [],
templateUrl: './hobbies.component.html',
styleUrl: './hobbies.component.scss',
})
export class HobbiesComponent {
}

View File

@@ -19,14 +19,14 @@ export class LanguageService {
use(l: Lang) {
this.lang.set(l);
this.translate.use(l);
try { localStorage.setItem(LocalStoreConstants.LANGUAGE_KEY, l); } catch {}
try { localStorage.setItem(LocalStoreConstants.LANGUAGE_KEY, l); } catch (e) { void e; }
}
private getInitial(): Lang {
try {
const stored = localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY) as Lang | null;
if (stored === 'de' || stored === 'en') return stored;
} catch {}
} catch (e) { void e; }
const browser = (navigator.language || 'en').toLowerCase();
return browser.startsWith('de') ? 'de' : 'en';
}

View File

@@ -10,13 +10,7 @@ export class ReloadService {
private readonly _languageChangedTick = signal(0);
readonly languageChangedTick = this._languageChangedTick.asReadonly();
constructor(zone: NgZone) {
zone.runOutsideAngular(() => {
globalThis.addEventListener('storage', (e: StorageEvent) => {
this.informListeners(e, zone);
});
});
}
private informListeners(e: StorageEvent, zone: NgZone) {

View File

@@ -20,7 +20,7 @@ export class ThemeService {
body.classList.toggle('dark', isDark);
overlayEl.classList.toggle('dark', isDark);
try { localStorage.setItem(LocalStoreConstants.THEME_KEY, this.theme()); } catch {}
try { localStorage.setItem(LocalStoreConstants.THEME_KEY, this.theme()); } catch (e) { void e; }
});
try {
@@ -29,7 +29,7 @@ export class ThemeService {
const stored = localStorage.getItem(LocalStoreConstants.THEME_KEY) as Theme | null;
if (!stored) this.setTheme(e.matches ? 'dark' : 'light');
});
} catch {}
} catch (e) { void e; }
}
toggle() { this.setTheme(this.theme() === 'dark' ? 'light' : 'dark'); }
@@ -39,10 +39,10 @@ export class ThemeService {
try {
const stored = localStorage.getItem(LocalStoreConstants.THEME_KEY) as Theme | null;
if (stored === 'dark' || stored === 'light') return stored;
} catch {}
} catch (e) { void e; }
try {
return globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} catch {}
} catch (e) { void e; }
return 'light';
}
}

View File

@@ -7,7 +7,7 @@
"ABOUT": "Über mich",
"IMPRINT": "Impressum",
"PROJECTS": "Projekte",
"HOBBY": "Hobbies",
"ALGORITHMS": "Algorithmen",
"SETTINGS": "Einstellungen",
"LANGUAGE": "Sprache",
"APPEARANCE": "Darstellung"
@@ -292,5 +292,24 @@
"PARAGRAPH": "Angaben gemäß § 5 DDG",
"COUNTRY": "Deutschland",
"CONTACT": "Kontakt"
},
"PATHFINDING": {
"TITLE": "Pfadfindungsalgorithmen",
"START_NODE": "Startknoten",
"END_NODE": "Endknoten",
"WALL": "Wand",
"CLEAR_NODE": "Löschen",
"DIJKSTRA": "Dijkstra",
"ASTAR": "A*",
"RESET_BOARD": "Board zurücksetzten",
"CLEAR_BOARD": "Board leeren",
"CLEAR_PATH": "Pfad löschen",
"VISITED": "Besucht",
"PATH": "Pfad",
"PATH_LENGTH": "Pfadlänge",
"EXECUTION_TIME": "Ausführungszeit",
"ALERT": {
"START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten."
}
}
}

View File

@@ -7,7 +7,7 @@
"ABOUT": "About me",
"IMPRINT": "Impressum",
"PROJECTS": "Projects",
"HOBBY": "Hobby's",
"ALGORITHMS": "Algorithms",
"SETTINGS": "Settings",
"LANGUAGE": "Language",
"APPEARANCE": "Appearance"
@@ -292,5 +292,24 @@
"PARAGRAPH": "Information pursuant to Section 5 DDG",
"COUNTRY": "Germany",
"CONTACT": "Contact"
},
"PATHFINDING": {
"TITLE": "Pathfinding Algorithms",
"START_NODE": "Start Node",
"END_NODE": "End Node",
"WALL": "Wall",
"CLEAR_NODE": "Clear",
"DIJKSTRA": "Dijkstra",
"ASTAR": "A*",
"RESET_BOARD": "Reset Board",
"CLEAR_BOARD": "Clear Board",
"CLEAR_PATH": "Clear Path",
"VISITED": "Visited",
"PATH": "Path",
"PATH_LENGTH": "Path length",
"EXECUTION_TIME": "Execution Time",
"ALERT": {
"START_END_NODES": "Please select a start and end node before running the algorithm."
}
}
}