From 17db9973987e4d2e8e2cd1f0ad07e244851ee0cc Mon Sep 17 00:00:00 2001 From: Lobo Date: Sun, 1 Feb 2026 16:00:10 +0100 Subject: [PATCH] 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. --- src/app/app.routes.ts | 6 +- src/app/layout/topbar/topbar.component.html | 6 +- src/app/pages/about/about.component.html | 2 +- .../algorithms/algorithms.component.html | 13 + .../algorithms/algorithms.component.scss | 19 ++ .../pages/algorithms/algorithms.component.ts | 26 ++ .../algorithms/models/algorithm-category.ts | 6 + .../pathfinding/pathfinding.component.html | 29 ++ .../pathfinding/pathfinding.component.scss | 49 +++ .../pathfinding/pathfinding.component.ts | 284 ++++++++++++++++++ .../pathfinding/pathfinding.models.ts | 16 + .../service/pathfinding.service.ts | 146 +++++++++ .../algorithms/service/algorithms.service.ts | 28 ++ src/app/pages/hobbies/hobbies.component.html | 3 - src/app/pages/hobbies/hobbies.component.scss | 19 -- src/app/pages/hobbies/hobbies.component.ts | 11 - src/assets/i18n/de.json | 18 +- src/assets/i18n/en.json | 18 +- 18 files changed, 658 insertions(+), 41 deletions(-) create mode 100644 src/app/pages/algorithms/algorithms.component.html create mode 100644 src/app/pages/algorithms/algorithms.component.scss create mode 100644 src/app/pages/algorithms/algorithms.component.ts create mode 100644 src/app/pages/algorithms/models/algorithm-category.ts create mode 100644 src/app/pages/algorithms/pathfinding/pathfinding.component.html create mode 100644 src/app/pages/algorithms/pathfinding/pathfinding.component.scss create mode 100644 src/app/pages/algorithms/pathfinding/pathfinding.component.ts create mode 100644 src/app/pages/algorithms/pathfinding/pathfinding.models.ts create mode 100644 src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts create mode 100644 src/app/pages/algorithms/service/algorithms.service.ts delete mode 100644 src/app/pages/hobbies/hobbies.component.html delete mode 100644 src/app/pages/hobbies/hobbies.component.scss delete mode 100644 src/app/pages/hobbies/hobbies.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 79294b4..464633b 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -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}, ]; diff --git a/src/app/layout/topbar/topbar.component.html b/src/app/layout/topbar/topbar.component.html index 1777ec1..bd00119 100644 --- a/src/app/layout/topbar/topbar.component.html +++ b/src/app/layout/topbar/topbar.component.html @@ -12,7 +12,7 @@ @@ -35,8 +35,8 @@ - + + + + + +
+ {{ 'PATHFINDING.START_NODE' | translate }} + {{ 'PATHFINDING.END_NODE' | translate }} + {{ 'PATHFINDING.WALL' | translate }} + {{ 'PATHFINDING.VISITED' | translate }} + {{ 'PATHFINDING.PATH' | translate }} +
+ + + + diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.scss b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss new file mode 100644 index 0000000..94346e3 --- /dev/null +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts new file mode 100644 index 0000000..8ea27ae --- /dev/null +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -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; + 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; + } +} diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts new file mode 100644 index 0000000..d55af22 --- /dev/null +++ b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts @@ -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 diff --git a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts new file mode 100644 index 0000000..dd19f39 --- /dev/null +++ b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts @@ -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; + } +} diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/service/algorithms.service.ts new file mode 100644 index 0000000..c84b802 --- /dev/null +++ b/src/app/pages/algorithms/service/algorithms.service.ts @@ -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 { + return of(this.categories); + } +} diff --git a/src/app/pages/hobbies/hobbies.component.html b/src/app/pages/hobbies/hobbies.component.html deleted file mode 100644 index 3e705ce..0000000 --- a/src/app/pages/hobbies/hobbies.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- > Work in progress -
diff --git a/src/app/pages/hobbies/hobbies.component.scss b/src/app/pages/hobbies/hobbies.component.scss deleted file mode 100644 index 07ff5e5..0000000 --- a/src/app/pages/hobbies/hobbies.component.scss +++ /dev/null @@ -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; } -} diff --git a/src/app/pages/hobbies/hobbies.component.ts b/src/app/pages/hobbies/hobbies.component.ts deleted file mode 100644 index 8dfb207..0000000 --- a/src/app/pages/hobbies/hobbies.component.ts +++ /dev/null @@ -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 { - -} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index efc920a..0c61726 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -7,7 +7,7 @@ "ABOUT": "Über mich", "IMPRINT": "Impressum", "PROJECTS": "Projekte", - "HOBBY": "Hobbies", + "ALGORITHMS": "Algorithmen", "SETTINGS": "Einstellungen", "LANGUAGE": "Sprache", "APPEARANCE": "Darstellung" @@ -292,5 +292,21 @@ "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*", + "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." + } } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 81d2fb2..d8c5241 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -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,21 @@ "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*", + "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." + } } }