Add algorithms section with pathfinding visualizer
Introduces a new 'Algorithms' section, replacing the previous 'Hobbies' page. Adds components, services, and models for algorithm categories and a pathfinding visualizer supporting Dijkstra and A* algorithms. Updates navigation and i18n files to reflect the new section and removes all hobbies-related files.
This commit is contained in:
@@ -1,14 +1,16 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import {AboutComponent} from './pages/about/about.component';
|
import {AboutComponent} from './pages/about/about.component';
|
||||||
import {ProjectsComponent} from './pages/projects/projects.component';
|
import {ProjectsComponent} from './pages/projects/projects.component';
|
||||||
import {HobbiesComponent} from './pages/hobbies/hobbies.component';
|
|
||||||
import {ImprintComponent} from './pages/imprint/imprint.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 = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: AboutComponent },
|
{ path: '', component: AboutComponent },
|
||||||
{ path: 'about', component: AboutComponent},
|
{ path: 'about', component: AboutComponent},
|
||||||
{ path: 'projects', component: ProjectsComponent},
|
{ path: 'projects', component: ProjectsComponent},
|
||||||
{ path: 'hobbies', component: HobbiesComponent},
|
{ path: 'algorithms', component: AlgorithmsComponent},
|
||||||
|
{ path: 'algorithms/pathfinding', component: PathfindingComponent },
|
||||||
{ path: 'imprint', component: ImprintComponent},
|
{ path: 'imprint', component: ImprintComponent},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a routerLink="/about" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
|
<a routerLink="/about" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
|
||||||
<a routerLink="/projects" mat-button>{{ 'TOPBAR.PROJECTS' | 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>
|
<a routerLink="/imprint" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
<button mat-menu-item routerLink="/projects">
|
<button mat-menu-item routerLink="/projects">
|
||||||
{{ 'TOPBAR.PROJECTS' | translate }}
|
{{ 'TOPBAR.PROJECTS' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/hobbys">
|
<button mat-menu-item routerLink="/algorithms">
|
||||||
{{ 'TOPBAR.HOBBY' | translate }}
|
{{ 'TOPBAR.ALGORITHMS' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/imprint">
|
<button mat-menu-item routerLink="/imprint">
|
||||||
{{ 'TOPBAR.IMPRINT' | translate }}
|
{{ 'TOPBAR.IMPRINT' | translate }}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-card class="experience">
|
<mat-card class="experdience">
|
||||||
<h2>{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
|
<h2>{{ '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) {
|
||||||
|
|||||||
13
src/app/pages/algorithms/algorithms.component.html
Normal file
13
src/app/pages/algorithms/algorithms.component.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h1>Algorithmen</h1>
|
||||||
|
<div class="category-cards">
|
||||||
|
<mat-card *ngFor="let category of categories$ | async" [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>
|
||||||
19
src/app/pages/algorithms/algorithms.component.scss
Normal file
19
src/app/pages/algorithms/algorithms.component.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/pages/algorithms/algorithms.component.ts
Normal file
26
src/app/pages/algorithms/algorithms.component.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, OnInit } 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 {
|
||||||
|
|
||||||
|
categories$: Observable<AlgorithmCategory[]> | undefined;
|
||||||
|
|
||||||
|
constructor(private algorithmsService: AlgorithmsService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.categories$ = this.algorithmsService.getCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/app/pages/algorithms/models/algorithm-category.ts
Normal file
6
src/app/pages/algorithms/models/algorithm-category.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface AlgorithmCategory {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
routerLink: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<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)="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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
284
src/app/pages/algorithms/pathfinding/pathfinding.component.ts
Normal file
284
src/app/pages/algorithms/pathfinding/pathfinding.component.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, HostListener, ViewChild} 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 {NgIf} from '@angular/common';
|
||||||
|
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, NgIf, TranslateModule],
|
||||||
|
templateUrl: './pathfinding.component.html',
|
||||||
|
styleUrls: ['./pathfinding.component.scss']
|
||||||
|
})
|
||||||
|
export class PathfindingComponent implements AfterViewInit {
|
||||||
|
|
||||||
|
@ViewChild('gridCanvas', { static: true })
|
||||||
|
canvas!: ElementRef<HTMLCanvasElement>;
|
||||||
|
ctx!: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
grid: Node[][] = [];
|
||||||
|
startNode: Node | null = null;
|
||||||
|
endNode: Node | null = null;
|
||||||
|
|
||||||
|
isDrawing: boolean = false;
|
||||||
|
selectedNodeType: NodeType = NodeType.None; // Default to no selection
|
||||||
|
animationSpeed: number = 10; // milliseconds
|
||||||
|
|
||||||
|
readonly NodeType = NodeType; // Expose enum to template
|
||||||
|
|
||||||
|
constructor(private pathfindingService: PathfindingService,
|
||||||
|
private translate: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
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(): 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[Math.floor(GRID_ROWS / 2)][Math.floor(GRID_COLS / 4)];
|
||||||
|
this.startNode.isStart = true;
|
||||||
|
this.endNode = this.grid[Math.floor(GRID_ROWS / 2)][Math.floor(3 * GRID_COLS / 4)];
|
||||||
|
this.endNode.isEnd = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
this.isDrawing = true;
|
||||||
|
this.placeNode(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event: MouseEvent): void {
|
||||||
|
if (this.isDrawing) {
|
||||||
|
this.placeNode(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(event: MouseEvent): void {
|
||||||
|
this.isDrawing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = this.grid[row][col];
|
||||||
|
|
||||||
|
switch (this.selectedNodeType) {
|
||||||
|
case NodeType.Start:
|
||||||
|
if (!node.isEnd && !node.isWall) {
|
||||||
|
if (this.startNode) {
|
||||||
|
this.startNode.isStart = false;
|
||||||
|
}
|
||||||
|
node.isStart = true;
|
||||||
|
this.startNode = node;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NodeType.End:
|
||||||
|
if (!node.isStart && !node.isWall) {
|
||||||
|
if (this.endNode) {
|
||||||
|
this.endNode.isEnd = false;
|
||||||
|
}
|
||||||
|
node.isEnd = true;
|
||||||
|
this.endNode = node;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NodeType.Wall:
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isWall = !node.isWall;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NodeType.None:
|
||||||
|
// Clear a node
|
||||||
|
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.drawGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
visualizeDijkstra(): void {
|
||||||
|
if (!this.startNode || !this.endNode) {
|
||||||
|
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clearPath();
|
||||||
|
const gridCopy = this.getCleanGrid();
|
||||||
|
const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.dijkstra(gridCopy,
|
||||||
|
gridCopy[this.startNode.row][this.startNode.col],
|
||||||
|
gridCopy[this.endNode.row][this.endNode.col]
|
||||||
|
);
|
||||||
|
this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
visualizeAStar(): void {
|
||||||
|
if (!this.startNode || !this.endNode) {
|
||||||
|
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clearPath();
|
||||||
|
const gridCopy = this.getCleanGrid();
|
||||||
|
const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.aStar(gridCopy,
|
||||||
|
gridCopy[this.startNode.row][this.startNode.col],
|
||||||
|
gridCopy[this.endNode.row][this.endNode.col]
|
||||||
|
);
|
||||||
|
this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
animateAlgorithm(visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[]): void {
|
||||||
|
for (let i = 0; i <= visitedNodesInOrder.length; i++) {
|
||||||
|
if (i === visitedNodesInOrder.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.animateShortestPath(nodesInShortestPathOrder);
|
||||||
|
}, this.animationSpeed * i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = visitedNodesInOrder[i];
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isVisited = true;
|
||||||
|
this.drawGrid();
|
||||||
|
}
|
||||||
|
}, this.animationSpeed * i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animateShortestPath(nodesInShortestPathOrder: Node[]): void {
|
||||||
|
for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
|
||||||
|
const node = nodesInShortestPathOrder[i];
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isPath = true;
|
||||||
|
this.drawGrid();
|
||||||
|
}
|
||||||
|
}, 50 * i); // Speed up path animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBoard(): void {
|
||||||
|
this.initializeGrid();
|
||||||
|
this.drawGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPath(): void {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get a deep copy of the grid for algorithm execution
|
||||||
|
private getCleanGrid(): Node[][] {
|
||||||
|
const newGrid: Node[][] = [];
|
||||||
|
for (let row = 0; row < GRID_ROWS; row++) {
|
||||||
|
const currentRow: Node[] = [];
|
||||||
|
for (let col = 0; col < GRID_COLS; col++) {
|
||||||
|
const node = this.grid[row][col];
|
||||||
|
currentRow.push({
|
||||||
|
...node,
|
||||||
|
isVisited: false,
|
||||||
|
isPath: false,
|
||||||
|
distance: Infinity,
|
||||||
|
previousNode: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
newGrid.push(currentRow);
|
||||||
|
}
|
||||||
|
return newGrid;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/pages/algorithms/pathfinding/pathfinding.models.ts
Normal file
16
src/app/pages/algorithms/pathfinding/pathfinding.models.ts
Normal 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 = 25;
|
||||||
|
export const GRID_COLS = 50;
|
||||||
|
export const NODE_SIZE = 20; // in pixels
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Node, GRID_ROWS, GRID_COLS } from '../pathfinding.models';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PathfindingService {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/pages/algorithms/service/algorithms.service.ts
Normal file
28
src/app/pages/algorithms/service/algorithms.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="terminal-loader">
|
|
||||||
<span>> Work in progress</span><span class="cursor"></span>
|
|
||||||
</div>
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"ABOUT": "Über mich",
|
"ABOUT": "Über mich",
|
||||||
"IMPRINT": "Impressum",
|
"IMPRINT": "Impressum",
|
||||||
"PROJECTS": "Projekte",
|
"PROJECTS": "Projekte",
|
||||||
"HOBBY": "Hobbies",
|
"ALGORITHMS": "Algorithmen",
|
||||||
"SETTINGS": "Einstellungen",
|
"SETTINGS": "Einstellungen",
|
||||||
"LANGUAGE": "Sprache",
|
"LANGUAGE": "Sprache",
|
||||||
"APPEARANCE": "Darstellung"
|
"APPEARANCE": "Darstellung"
|
||||||
@@ -292,5 +292,21 @@
|
|||||||
"PARAGRAPH": "Angaben gemäß § 5 DDG",
|
"PARAGRAPH": "Angaben gemäß § 5 DDG",
|
||||||
"COUNTRY": "Deutschland",
|
"COUNTRY": "Deutschland",
|
||||||
"CONTACT": "Kontakt"
|
"CONTACT": "Kontakt"
|
||||||
|
},
|
||||||
|
"PATHFINDING": {
|
||||||
|
"TITLE": "Pfadfindungsalgorithmen",
|
||||||
|
"START_NODE": "Startknoten",
|
||||||
|
"END_NODE": "Endknoten",
|
||||||
|
"WALL": "Wand",
|
||||||
|
"CLEAR_NODE": "Löschen",
|
||||||
|
"DIJKSTRA": "Dijkstra",
|
||||||
|
"ASTAR": "A*",
|
||||||
|
"CLEAR_BOARD": "Board leeren",
|
||||||
|
"CLEAR_PATH": "Pfad löschen",
|
||||||
|
"VISITED": "Besucht",
|
||||||
|
"PATH": "Pfad",
|
||||||
|
"ALERT": {
|
||||||
|
"START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"ABOUT": "About me",
|
"ABOUT": "About me",
|
||||||
"IMPRINT": "Impressum",
|
"IMPRINT": "Impressum",
|
||||||
"PROJECTS": "Projects",
|
"PROJECTS": "Projects",
|
||||||
"HOBBY": "Hobby's",
|
"ALGORITHMS": "Algorithms",
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
"LANGUAGE": "Language",
|
"LANGUAGE": "Language",
|
||||||
"APPEARANCE": "Appearance"
|
"APPEARANCE": "Appearance"
|
||||||
@@ -292,5 +292,21 @@
|
|||||||
"PARAGRAPH": "Information pursuant to Section 5 DDG",
|
"PARAGRAPH": "Information pursuant to Section 5 DDG",
|
||||||
"COUNTRY": "Germany",
|
"COUNTRY": "Germany",
|
||||||
"CONTACT": "Contact"
|
"CONTACT": "Contact"
|
||||||
|
},
|
||||||
|
"PATHFINDING": {
|
||||||
|
"TITLE": "Pathfinding Algorithms",
|
||||||
|
"START_NODE": "Start Node",
|
||||||
|
"END_NODE": "End Node",
|
||||||
|
"WALL": "Wall",
|
||||||
|
"CLEAR_NODE": "Clear",
|
||||||
|
"DIJKSTRA": "Dijkstra",
|
||||||
|
"ASTAR": "A*",
|
||||||
|
"CLEAR_BOARD": "Clear Board",
|
||||||
|
"CLEAR_PATH": "Clear Path",
|
||||||
|
"VISITED": "Visited",
|
||||||
|
"PATH": "Path",
|
||||||
|
"ALERT": {
|
||||||
|
"START_END_NODES": "Please select a start and end node before running the algorithm."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user