Compare commits

...

6 Commits

Author SHA1 Message Date
ac937eb2fb Merge pull request 'feature/cleanup' (#5) from feature/cleanup into main
All checks were successful
Build & Push Frontend A / docker (push) Successful in 42s
Reviewed-on: #5
2026-02-02 10:07:26 +01:00
e0f0a0ed04 Enhance pathfinding UI with grid resizing and scenarios
Added controls for dynamic grid size adjustment and scenario presets (normal and edge case) to the pathfinding component. Improved UI/UX with algorithm explanations, Wikipedia links, and reorganized controls. Refactored grid logic for flexibility, updated translations, and improved code structure for maintainability.
2026-02-02 10:06:59 +01:00
17a787b0f1 Small refactoring for better readabilty 2026-02-02 09:28:05 +01:00
01f66d4b8f Fixed smaller rounting bugs 2026-02-02 08:56:51 +01:00
3a13d62c9e Remove particles background and update routing
Deleted the particles background component and its related files. Updated routing logic and constants, refactored topbar and algorithms pages, and performed dependency updates and cleanup in package files. Also improved i18n translations and adjusted TypeScript configuration.
2026-02-02 08:51:10 +01:00
05fc70e583 Update package-lock.json 2026-02-02 07:43:53 +01:00
24 changed files with 1599 additions and 6958 deletions

7323
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "playground-frontend",
"version": "0.1.0",
"version": "0.2.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -11,7 +11,6 @@
},
"private": true,
"dependencies": {
"@angular-devkit/build-angular": "~21.1.0",
"@angular/animations": "~21.1.0",
"@angular/cdk": "~21.1.0",
"@angular/common": "~21.1.0",
@@ -23,18 +22,14 @@
"@angular/router": "~21.1.0",
"@ngx-translate/core": "~17.0.0",
"@ngx-translate/http-loader": "~17.0.0",
"@tsparticles/angular": "~3.0.0",
"@tsparticles/engine": "~3.9.1",
"rxjs": "~7.8.2",
"swiper": "~12.0.3",
"tslib": "~2.8.1",
"tsparticles": "~3.9.1"
"swiper": "~12.1.0",
"tslib": "~2.8.1"
},
"devDependencies": {
"@angular/build": "~21.1.0",
"@angular/cli": "~21.1.0",
"@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",

View File

@@ -1,16 +1,13 @@
import { Routes } from '@angular/router';
import {AboutComponent} from './pages/about/about.component';
import {ProjectsComponent} from './pages/projects/projects.component';
import {ImprintComponent} from './pages/imprint/imprint.component';
import {AlgorithmsComponent} from './pages/algorithms/algorithms.component';
import {PathfindingComponent} from './pages/algorithms/pathfinding/pathfinding.component';
import {RouterConstants} from './constants/RouterConstants';
export const routes: Routes = [
{ path: '', component: AboutComponent },
{ path: 'about', component: AboutComponent},
{ path: 'projects', component: ProjectsComponent},
{ path: 'algorithms', component: AlgorithmsComponent},
{ path: 'algorithms/pathfinding', component: PathfindingComponent },
{ path: 'imprint', component: ImprintComponent},
{ path: RouterConstants.ABOUT.PATH, component: RouterConstants.ABOUT.COMPONENT},
{ path: RouterConstants.PROJECTS.PATH, component: RouterConstants.PROJECTS.COMPONENT},
{ path: RouterConstants.ALGORITHMS.PATH, component: RouterConstants.ALGORITHMS.COMPONENT},
{ path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT},
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
];

View File

@@ -0,0 +1,39 @@
import {AboutComponent} from '../pages/about/about.component';
import {ProjectsComponent} from '../pages/projects/projects.component';
import {ImprintComponent} from '../pages/imprint/imprint.component';
import {AlgorithmsComponent} from '../pages/algorithms/algorithms.component';
import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.component';
export class RouterConstants {
static readonly ABOUT = {
PATH: 'about',
LINK: '/about',
COMPONENT: AboutComponent
};
static readonly PROJECTS = {
PATH: 'projects',
LINK: '/projects',
COMPONENT: ProjectsComponent
};
static readonly ALGORITHMS = {
PATH: 'algorithms',
LINK: '/algorithms',
COMPONENT: AlgorithmsComponent
};
static readonly PATHFINDING = {
PATH: 'algorithms/pathfinding',
LINK: '/algorithms/pathfinding',
COMPONENT: PathfindingComponent
};
static readonly IMPRINT = {
PATH: 'imprint',
LINK: '/imprint',
COMPONENT: ImprintComponent
};
}

View File

@@ -1,4 +1,6 @@
export class UrlConstants {
static readonly LINKED_IN = 'https://www.linkedin.com/in/andreas-dahm-2395991ba';
static readonly GIT_HUB = 'https://github.com/LoboTheDark';
static readonly DIJKSTRA_WIKI = 'https://de.wikipedia.org/wiki/Dijkstra-Algorithmus'
static readonly ASTAR_WIKI = 'https://de.wikipedia.org/wiki/A*-Algorithmus'
}

View File

@@ -1,6 +0,0 @@
<ngx-particles
[id]="id"
[options]="particlesOptions"
[particlesInit]="particlesInit"
(particlesLoaded)="particlesLoaded($event)"
></ngx-particles>

View File

@@ -1,2 +0,0 @@
:host { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.particles { width: 100%; height: 100%; display: block; }

View File

@@ -1,104 +0,0 @@
import {
Container,
Engine
} from "@tsparticles/engine";
import {NgParticlesService, NgxParticlesModule} from "@tsparticles/angular";
import { Component, inject } from '@angular/core';
import {loadFull} from 'tsparticles';
@Component({
selector: 'app-particles-bg',
standalone: true,
imports: [
NgxParticlesModule
],
templateUrl: './particles-bg.component.html',
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 */
particlesUrl = "http://foo.bar/particles.json";
/* or the classic JavaScript object */
particlesOptions = {
/*background: {
color: {
value: "#0d47a1",
},
},
fpsLimit: 120,
interactivity: {
events: {
onClick: {
enable: true,
},
onHover: {
enable: true,
},
resize: true,
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: "#ffffff",
},
links: {
color: "#ffffff",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
move: {
direction: MoveDirection.none,
enable: true,
outModes: {
default: OutMode.bounce,
},
random: false,
speed: 6,
straight: false,
},
number: {
density: {
enable: true,
area: 800,
},
value: 80,
},
opacity: {
value: 0.5,
},
shape: {
type: "circle",
},
size: {
value: { min: 1, max: 5 },
},
},
detectRetina: true,*/
};
async particlesInit(engine: Engine): Promise<void> {
await loadFull(engine);
}
particlesLoaded(container: Container): void {
console.log(container);
}
}

View File

@@ -10,10 +10,10 @@
</a>
<nav class="nav">
<a routerLink="/about" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
<a routerLink="/projects" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
<a routerLink="/algorithms" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
<a routerLink="/imprint" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
<a [routerLink]="RouterConstants.ABOUT.LINK" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
<a [routerLink]="RouterConstants.PROJECTS.LINK" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
<a [routerLink]="RouterConstants.ALGORITHMS.LINK" mat-button>{{ 'TOPBAR.ALGORITHMS' | translate }}</a>
<a [routerLink]="RouterConstants.IMPRINT.LINK" mat-button>{{ 'TOPBAR.IMPRINT' | translate }}</a>
</nav>
<!-- Mobile nav menu button -->
@@ -29,16 +29,16 @@
<!-- Mobile nav menu -->
<mat-menu #navMenu="matMenu" xPosition="before">
<button mat-menu-item routerLink="/about">
<button mat-menu-item [routerLink]="RouterConstants.ABOUT.LINK">
{{ 'TOPBAR.ABOUT' | translate }}
</button>
<button mat-menu-item routerLink="/projects">
<button mat-menu-item [routerLink]="RouterConstants.PROJECTS.LINK">
{{ 'TOPBAR.PROJECTS' | translate }}
</button>
<button mat-menu-item routerLink="/algorithms">
<button mat-menu-item [routerLink]="RouterConstants.ALGORITHMS.LINK">
{{ 'TOPBAR.ALGORITHMS' | translate }}
</button>
<button mat-menu-item routerLink="/imprint">
<button mat-menu-item [routerLink]="RouterConstants.IMPRINT.LINK">
{{ 'TOPBAR.IMPRINT' | translate }}
</button>
</mat-menu>

View File

@@ -10,6 +10,7 @@ import { ThemeService } from '../../service/theme.service';
import { LanguageService } from '../../service/language.service';
import { MatDivider } from '@angular/material/divider';
import {AssetsConstants} from '../../constants/AssetsConstants';
import {RouterConstants} from '../../constants/RouterConstants';
@Component({
selector: 'app-topbar',
@@ -41,4 +42,5 @@ export class TopbarComponent {
setLang(code: 'de' | 'en') { this.lang.use(code); }
protected readonly AssetsConstants = AssetsConstants;
protected readonly RouterConstants = RouterConstants;
}

View File

@@ -1,13 +1,13 @@
<div class="container">
<h1>Algorithmen</h1>
<h1>{{ 'ALGORITHM.TITLE' |translate }}</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-title>{{ category.title | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>{{ category.description }}</p>
<p>{{ category.description | translate}}</p>
</mat-card-content>
</mat-card>
}

View File

@@ -10,10 +10,7 @@
mat-card {
cursor: pointer;
max-width: 300px;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
min-width: 300px;
max-width: 400px;
}
}

View File

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

View File

@@ -1,6 +1,20 @@
<div class="container">
<h1>{{ 'PATHFINDING.TITLE' | translate }}</h1>
<div class="algo-info">
<h3>{{ 'PATHFINDING.EXPLANATION.TITLE' | translate }}</h3>
<p>
<strong>Dijkstra</strong> {{ 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION' | translate }}
<a href="{{UrlConstants.DIJKSTRA_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
</p>
<p>
<strong>A*</strong> {{ 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION' | translate}}
<a href="{{UrlConstants.ASTAR_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
</p>
</div>
<div class="controls-container">
<div class="controls">
<mat-button-toggle-group [(ngModel)]="selectedNodeType" aria-label="Node Type Selection">
@@ -9,11 +23,44 @@
<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>
</div>
<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>
<div class="controls">
<button matButton="filled" (click)="visualizeDijkstra()">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
<button matButton="filled" (click)="visualizeAStar()">{{ 'PATHFINDING.ASTAR' | translate }}</button>
<button matButton="filled" (click)="normalCase()">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
<button matButton="filled" (click)="edgeCase()">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button>
<button matButton="filled" (click)="clearBoard()">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
</div>
<div class="controls">
<div class="grid-size">
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'ALGORITHM.PATHFINDING.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
type="number"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
[(ngModel)]="gridRows"
(blur)="applyGridSize()"
(keyup.enter)="applyGridSize()"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'ALGORITHM.PATHFINDING.GRID_WIDTH' | translate }}</mat-label>
<input
matInput
type="number"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
[(ngModel)]="gridCols"
(blur)="applyGridSize()"
(keyup.enter)="applyGridSize()"
/>
</mat-form-field>
</div>
</div>
<div class="legend">
@@ -25,10 +72,10 @@
</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>
<canvas #gridCanvas></canvas>
</div>

View File

@@ -2,6 +2,25 @@
padding: 2rem;
}
.algo-info {
margin: 0 0 1rem 0;
padding: 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 8px;
h3 {
margin: 0 0 0.5rem 0;
}
p {
margin: 0.5rem 0;
}
a {
margin-left: 0.25rem;
}
}
.controls-container {
display: flex;
flex-direction: column;
@@ -13,6 +32,7 @@
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
align-items: center;
mat-button-toggle-group {
border-radius: 4px;
@@ -20,6 +40,17 @@
}
}
.grid-size {
display: flex;
gap: 0.75rem;
align-items: center;
flex-wrap: wrap;
}
.grid-field {
width: 150px;
}
.legend {
display: flex;
flex-wrap: wrap;
@@ -46,4 +77,5 @@
canvas {
border: 1px solid #ccc;
display: block;
max-width: 100%;
}

View File

@@ -1,13 +1,18 @@
import { AfterViewInit, Component, ElementRef, ViewChild, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import {GRID_COLS, GRID_ROWS, NODE_SIZE, Node} from './pathfinding.models';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import { PathfindingService } from './service/pathfinding.service';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
// Define an enum for node types that can be placed by the user
import {MatButtonModule} from '@angular/material/button';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, Node} from './pathfinding.models';
import {PathfindingService} from './service/pathfinding.service';
import {UrlConstants} from '../../../constants/UrlConstants';
enum NodeType {
Start = 'start',
End = 'end',
@@ -15,334 +20,295 @@ enum NodeType {
None = 'none'
}
interface GridPos { row: number; col: number }
@Component({
selector: 'app-pathfinding',
standalone: true,
imports: [CommonModule, MatButtonModule, MatButtonToggleModule, FormsModule, TranslateModule],
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatButtonToggleModule,
MatFormFieldModule,
MatInputModule,
TranslateModule
],
templateUrl: './pathfinding.component.html',
styleUrls: ['./pathfinding.component.scss']
})
export class PathfindingComponent implements AfterViewInit {
private readonly pathfindingService = inject(PathfindingService);
private readonly translate = inject(TranslateService);
private lastRow = -1;
private lastCol = -1;
private timeoutIds: any[] = [];
readonly NodeType = NodeType;
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
@ViewChild('gridCanvas', { static: true })
canvas!: ElementRef<HTMLCanvasElement>;
ctx!: CanvasRenderingContext2D;
private ctx!: CanvasRenderingContext2D;
gridRows = DEFAULT_GRID_ROWS;
gridCols = DEFAULT_GRID_COLS;
nodeSize = 10;
grid: Node[][] = [];
startNode: Node | null = null;
endNode: Node | null = null;
selectedNodeType: NodeType = NodeType.None;
isDrawing = false;
shouldAddWall = true;
selectedNodeType: NodeType = NodeType.None; // Default to no selection
animationSpeed = 3; // milliseconds
private lastCell: GridPos | null = null;
private shouldAddWall = true;
animationSpeed = 3;
pathLength = 0;
executionTime = 0;
readonly NodeType = NodeType;
private timeoutIds: number[] = [];
ngAfterViewInit(): void {
this.ctx = this.canvas.nativeElement.getContext('2d') as CanvasRenderingContext2D;
this.canvas.nativeElement.width = GRID_COLS * NODE_SIZE;
this.canvas.nativeElement.height = GRID_ROWS * NODE_SIZE;
this.initializeGrid(true);
this.ctx = this.getContextOrThrow();
this.applyGridSize(true);
const el = this.canvas.nativeElement;
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
el.addEventListener('mouseup', () => this.onMouseUp());
el.addEventListener('mouseleave', () => this.onMouseUp());
}
applyGridSize(skipReset?: boolean): void {
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
this.resizeCanvas();
if (skipReset) {
this.initializeGrid(true, 'edge');
this.drawGrid();
return;
}
// Default after size changes: pick one consistent scenario
this.edgeCase();
}
// Scenarios (buttons)
normalCase(): void {
this.stopAnimations();
this.initializeGrid(true, 'normal');
this.drawGrid();
// Add event listeners for mouse interactions
this.canvas.nativeElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.canvas.nativeElement.addEventListener('mousemove', this.onMouseMove.bind(this));
this.canvas.nativeElement.addEventListener('mouseup', this.onMouseUp.bind(this));
this.canvas.nativeElement.addEventListener('mouseleave', this.onMouseUp.bind(this)); // Stop drawing if mouse leaves canvas
}
initializeGrid(withWalls: boolean): void {
this.grid = [];
for (let row = 0; row < GRID_ROWS; row++) {
const currentRow: Node[] = [];
for (let col = 0; col < GRID_COLS; col++) {
currentRow.push({
row,
col,
isStart: false,
isEnd: false,
isWall: false,
isVisited: false,
isPath: false,
distance: Infinity,
previousNode: null,
fScore: 0
});
}
this.grid.push(currentRow);
}
// Set default start and end nodes
this.startNode = this.grid[0][Math.floor(GRID_COLS / 2)];
this.startNode.isStart = true;
this.endNode = this.grid[this.grid.length-1][Math.floor(GRID_COLS / 2)];
this.endNode.isEnd = true;
if (withWalls)
{
//setting walls
let offset = Math.floor(GRID_COLS / 4);
for (let startWall = 0; startWall < Math.floor(GRID_COLS /2 ); startWall++){
this.grid[Math.floor(GRID_ROWS / 2)][offset + startWall].isWall = true;
}
}
}
stopAnimations(): void {
this.timeoutIds.forEach((id) => clearTimeout(id));
this.timeoutIds = [];
}
drawGrid(): void {
if (!this.ctx) {
return;
}
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
for (let row = 0; row < GRID_ROWS; row++) {
for (let col = 0; col < GRID_COLS; col++) {
const node = this.grid[row][col];
let color = 'lightgray'; // Default color
if (node.isStart) {
color = 'green';
} else if (node.isEnd) {
color = 'red';
} else if (node.isPath) {
color = 'gold';
} else if (node.isVisited) {
color = 'skyblue';
} else if (node.isWall) {
color = 'black';
}
this.ctx.fillStyle = color;
this.ctx.fillRect(col * NODE_SIZE, row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
this.ctx.strokeStyle = '#ccc';
this.ctx.strokeRect(col * NODE_SIZE, row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
}
}
}
onMouseDown(event: MouseEvent): void {
const { row, col } = this.getGridPosition(event);
if (this.isValidPosition(row, col)) {
const node = this.grid[row][col];
this.shouldAddWall = !node.isWall;
}
this.isDrawing = true;
this.placeNode(event);
}
onMouseMove(event: MouseEvent): void {
if (this.isDrawing) {
this.placeNode(event);
}
}
getGridPosition(event: MouseEvent): { row: number, col: number } {
const rect = this.canvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / NODE_SIZE);
const row = Math.floor(y / NODE_SIZE);
return { row, col };
}
isValidPosition(row: number, col: number): boolean {
return row >= 0 && row < GRID_ROWS && col >= 0 && col < GRID_COLS;
}
onMouseUp(): void {
this.isDrawing = false;
this.lastRow = -1;
this.lastCol = -1;
}
placeNode(event: MouseEvent): void {
const rect = this.canvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / NODE_SIZE);
const row = Math.floor(y / NODE_SIZE);
if (row < 0 || row >= GRID_ROWS || col < 0 || col >= GRID_COLS) {
return;
}
if (this.lastRow === row && this.lastCol === col) {
return;
}
this.lastRow = row;
this.lastCol = col;
const node = this.grid[row][col];
switch (this.selectedNodeType) {
case NodeType.Start:
if (!node.isEnd && !node.isWall) {
if (this.startNode) {
this.startNode.isStart = false;
this.drawNode(this.startNode);
}
node.isStart = true;
this.startNode = node;
}
break;
case NodeType.End:
if (!node.isStart && !node.isWall) {
if (this.endNode) {
this.endNode.isEnd = false;
this.drawNode(this.endNode);
}
node.isEnd = true;
this.endNode = node;
}
break;
case NodeType.Wall:
if (!node.isStart && !node.isEnd) {
if (node.isWall !== this.shouldAddWall) {
node.isWall = this.shouldAddWall;
}
}
break;
case NodeType.None:
if (node.isStart) {
node.isStart = false;
this.startNode = null;
} else if (node.isEnd) {
node.isEnd = false;
this.endNode = null;
} else if (node.isWall) {
node.isWall = false;
}
break;
}
this.drawNode(node);
}
visualizeDijkstra(): void {
edgeCase(): void {
this.stopAnimations();
if (!this.startNode || !this.endNode) {
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
return;
}
this.clearPath();
const startTime = performance.now();
const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.dijkstra(this.grid,
this.grid[this.startNode.row][this.startNode.col],
this.grid[this.endNode.row][this.endNode.col]
);
const endTime = performance.now();
this.pathLength = nodesInShortestPathOrder.length;
this.executionTime = endTime - startTime;
this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
}
visualizeAStar(): void {
this.stopAnimations();
if (!this.startNode || !this.endNode) {
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
return;
}
this.clearPath();
const startTime = performance.now();
const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.aStar(this.grid,
this.grid[this.startNode.row][this.startNode.col],
this.grid[this.endNode.row][this.endNode.col]
);
const endTime = performance.now();
this.pathLength = nodesInShortestPathOrder.length;
this.executionTime = endTime - startTime;
this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
}
animateAlgorithm(visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[]): void {
for (let i = 0; i <= visitedNodesInOrder.length; i++) {
if (i === visitedNodesInOrder.length) {
const timeoutId = setTimeout(() => {
this.animateShortestPath(nodesInShortestPathOrder);
}, this.animationSpeed * i);
this.timeoutIds.push(timeoutId);
return;
}
const node = visitedNodesInOrder[i];
const timeoutId = setTimeout(() => {
if (!node.isStart && !node.isEnd) {
node.isVisited = true;
this.drawNode(node);
}
}, this.animationSpeed * i);
this.timeoutIds.push(timeoutId);
}
}
animateShortestPath(nodesInShortestPathOrder: Node[]): void {
for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
const node = nodesInShortestPathOrder[i];
const timeoutId = setTimeout(() => {
if (!node.isStart && !node.isEnd) {
node.isPath = true;
this.drawNode(node);
}
}, this.animationSpeed * i);
this.timeoutIds.push(timeoutId);
}
}
drawNode(node: Node): void {
if (!this.ctx) return;
let color = 'lightgray';
if (node.isStart) color = 'green';
else if (node.isEnd) color = 'red';
else if (node.isPath) color = 'gold';
else if (node.isVisited) color = 'skyblue';
else if (node.isWall) color = 'black';
this.ctx.fillStyle = color;
this.ctx.fillRect(node.col * NODE_SIZE, node.row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
this.ctx.strokeStyle = '#ccc';
this.ctx.strokeRect(node.col * NODE_SIZE, node.row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
}
resetBoard(): void {
this.stopAnimations();
this.initializeGrid(true);
this.initializeGrid(true, 'edge');
this.drawGrid();
}
clearBoard(): void {
this.stopAnimations();
this.initializeGrid(false);
this.initializeGrid(false, 'edge');
this.drawGrid();
}
clearPath(): void {
visualizeDijkstra(): void {
if (!this.ensureStartAndEnd()) {
return;
}
this.stopAnimations();
for (let row = 0; row < GRID_ROWS; row++) {
for (let col = 0; col < GRID_COLS; col++) {
this.clearPath();
const startTime = performance.now();
const result = this.pathfindingService.dijkstra(
this.grid,
this.grid[this.startNode!.row][this.startNode!.col],
this.grid[this.endNode!.row][this.endNode!.col]
);
const endTime = performance.now();
this.pathLength = result.nodesInShortestPathOrder.length;
this.executionTime = endTime - startTime;
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
}
visualizeAStar(): void {
if (!this.ensureStartAndEnd()) {
return;
}
this.stopAnimations();
this.clearPath();
const startTime = performance.now();
const result = this.pathfindingService.aStar(
this.grid,
this.grid[this.startNode!.row][this.startNode!.col],
this.grid[this.endNode!.row][this.endNode!.col]
);
const endTime = performance.now();
this.pathLength = result.nodesInShortestPathOrder.length;
this.executionTime = endTime - startTime;
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
}
// Mouse interactions
private onMouseDown(event: MouseEvent): void {
const pos = this.getGridPosition(event);
if (!pos) {
return;
}
this.shouldAddWall = this.shouldStartWallStroke(pos);
this.isDrawing = true;
this.lastCell = null;
this.applySelectionAt(pos);
}
private onMouseMove(event: MouseEvent): void {
if (!this.isDrawing) {
return;
}
const pos = this.getGridPosition(event);
if (!pos) {
return;
}
if (this.isSameCell(pos, this.lastCell)) {
return;
}
this.applySelectionAt(pos);
}
private onMouseUp(): void {
this.isDrawing = false;
this.lastCell = null;
}
private applySelectionAt(pos: GridPos): void {
const node = this.grid[pos.row][pos.col];
switch (this.selectedNodeType) {
case NodeType.Start:
this.trySetStart(node);
break;
case NodeType.End:
this.trySetEnd(node);
break;
case NodeType.Wall:
this.tryToggleWall(node, this.shouldAddWall);
break;
case NodeType.None:
this.tryClearNode(node);
break;
}
this.lastCell = pos;
this.drawNode(node);
}
// Grid init
private initializeGrid(withWalls: boolean, scenario: 'normal' | 'edge'): void {
this.grid = this.createEmptyGrid();
const { start, end } = this.getScenarioStartEnd(scenario);
this.startNode = this.grid[start.row][start.col];
this.endNode = this.grid[end.row][end.col];
this.startNode.isStart = true;
this.endNode.isEnd = true;
if (withWalls) {
this.placeDefaultDiagonalWall();
}
}
private createEmptyGrid(): Node[][] {
const grid: Node[][] = [];
for (let row = 0; row < this.gridRows; row++) {
const currentRow: Node[] = [];
for (let col = 0; col < this.gridCols; col++) {
currentRow.push(this.createNode(row, col));
}
grid.push(currentRow);
}
return grid;
}
private createNode(row: number, col: number): Node {
return {
row,
col,
isStart: false,
isEnd: false,
isWall: false,
isVisited: false,
isPath: false,
distance: Infinity,
previousNode: null,
fScore: 0
};
}
private getScenarioStartEnd(scenario: 'normal' | 'edge'): { start: GridPos; end: GridPos } {
if (scenario === 'edge') {
return {
start: { row: 0, col: 0 },
end: { row: this.gridRows - 1, col: this.gridCols - 1 }
};
}
// normal: mid-left -> mid-right
const midRow = Math.floor(this.gridRows / 2);
return {
start: { row: midRow, col: 0 },
end: { row: midRow, col: this.gridCols - 1 }
};
}
private placeDefaultDiagonalWall(): void {
// Diagonal-ish wall; avoids start/end
const len = Math.min(this.gridRows, this.gridCols);
const startCol = Math.floor((this.gridCols - len) / 2);
for (let i = 0; i < Math.max(0, len - 10); i++) {
const row = len - i - 1;
const col = startCol + i;
if (!this.isValidPosition(row, col)) {
continue;
}
const node = this.grid[row][col];
if (node.isStart || node.isEnd) {
continue;
}
node.isWall = true;
}
}
// Path state
private clearPath(): void {
for (let row = 0; row < this.gridRows; row++) {
for (let col = 0; col < this.gridCols; col++) {
const node = this.grid[row][col];
node.isVisited = false;
node.isPath = false;
@@ -353,4 +319,211 @@ export class PathfindingComponent implements AfterViewInit {
this.drawGrid();
}
// Animation
private stopAnimations(): void {
for (const id of this.timeoutIds) {
clearTimeout(id);
}
this.timeoutIds = [];
}
private animateAlgorithm(visited: Node[], path: Node[]): void {
for (let i = 0; i <= visited.length; i++) {
if (i === visited.length) {
const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
this.timeoutIds.push(id);
return;
}
const node = visited[i];
const id = globalThis.setTimeout(() => {
if (!node.isStart && !node.isEnd) {
node.isVisited = true;
this.drawNode(node);
}
}, this.animationSpeed * i);
this.timeoutIds.push(id);
}
}
private animateShortestPath(path: Node[]): void {
for (let i = 0; i < path.length; i++) {
const node = path[i];
const id = globalThis.setTimeout(() => {
if (!node.isStart && !node.isEnd) {
node.isPath = true;
this.drawNode(node);
}
}, this.animationSpeed * i);
this.timeoutIds.push(id);
}
}
// Drawing
private drawGrid(): void {
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
for (let row = 0; row < this.gridRows; row++) {
for (let col = 0; col < this.gridCols; col++) {
this.drawNode(this.grid[row][col]);
}
}
}
private drawNode(node: Node): void {
this.ctx.fillStyle = this.getNodeColor(node);
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
this.ctx.strokeStyle = '#ccc';
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
}
private getNodeColor(node: Node): string {
if (node.isStart) return 'green';
if (node.isEnd) return 'red';
if (node.isPath) return 'gold';
if (node.isVisited) return 'skyblue';
if (node.isWall) return 'black';
return 'lightgray';
}
// Placement rules (readability helpers)
private trySetStart(node: Node): void {
if (!this.canBeStart(node)) {
return;
}
if (this.startNode) {
this.startNode.isStart = false;
this.drawNode(this.startNode);
}
node.isStart = true;
this.startNode = node;
}
private trySetEnd(node: Node): void {
if (!this.canBeEnd(node)) {
return;
}
if (this.endNode) {
this.endNode.isEnd = false;
this.drawNode(this.endNode);
}
node.isEnd = true;
this.endNode = node;
}
private tryToggleWall(node: Node, shouldBeWall: boolean): void {
if (!this.canBeWall(node)) {
return;
}
node.isWall = shouldBeWall;
}
private tryClearNode(node: Node): void {
if (node.isStart) {
node.isStart = false;
this.startNode = null;
return;
}
if (node.isEnd) {
node.isEnd = false;
this.endNode = null;
return;
}
if (node.isWall) {
node.isWall = false;
}
}
private canBeStart(node: Node): boolean {
return !node.isEnd && !node.isWall;
}
private canBeEnd(node: Node): boolean {
return !node.isStart && !node.isWall;
}
private canBeWall(node: Node): boolean {
return !node.isStart && !node.isEnd;
}
private shouldStartWallStroke(pos: GridPos): boolean {
if (this.selectedNodeType !== NodeType.Wall) {
return true;
}
const node = this.grid[pos.row][pos.col];
return !node.isWall;
}
// Validation
private ensureStartAndEnd(): boolean {
if (this.startNode && this.endNode) {
return true;
}
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
return false;
}
// Grid sizing
private clampGridSize(value: number, fallback: number): number {
const parsed = Math.floor(Number(value));
const safe = Number.isFinite(parsed) ? parsed : fallback;
return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE);
}
private computeNodeSize(rows: number, cols: number): number {
const sizeByWidth = Math.floor(MAX_GRID_PX / cols);
const sizeByHeight = Math.floor(MAX_GRID_PX / rows);
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
}
private resizeCanvas(): void {
const el = this.canvas.nativeElement;
el.width = this.gridCols * this.nodeSize;
el.height = this.gridRows * this.nodeSize;
}
// Mouse -> grid cell
private getGridPosition(event: MouseEvent): GridPos | null {
const rect = this.canvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / this.nodeSize);
const row = Math.floor(y / this.nodeSize);
if (!this.isValidPosition(row, col)) {
return null;
}
return { row, col };
}
private isValidPosition(row: number, col: number): boolean {
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
}
private isSameCell(a: GridPos, b: GridPos | null): boolean {
return !!b && a.row === b.row && a.col === b.col;
}
private getContextOrThrow(): CanvasRenderingContext2D {
const ctx = this.canvas.nativeElement.getContext('2d');
if (!ctx) {
throw new Error('CanvasRenderingContext2D not available.');
}
return ctx;
}
protected readonly UrlConstants = UrlConstants;
}

View File

@@ -11,6 +11,11 @@ export interface Node {
fScore: number;
}
export const GRID_ROWS = 150;
export const GRID_COLS = 100;
export const NODE_SIZE = 10; // in pixels
export const DEFAULT_GRID_ROWS = 50;
export const DEFAULT_GRID_COLS = 50;
export const MIN_GRID_SIZE = 2;
export const MAX_GRID_SIZE = 150;
// Canvas max size (px)
export const MAX_GRID_PX = 1000;

View File

@@ -1,22 +1,20 @@
import { Injectable } from '@angular/core';
import { Node, GRID_ROWS, GRID_COLS } from '../pathfinding.models';
import { Node} 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 (row < grid.length - 1) neighbors.push(grid[row + 1][col]);
if (col > 0) neighbors.push(grid[row][col - 1]);
if (col < GRID_COLS - 1) neighbors.push(grid[row][col + 1]);
if (col < grid[0].length - 1) neighbors.push(grid[row][col + 1]);
return neighbors.filter(neighbor => !neighbor.isVisited && !neighbor.isWall);
}
@@ -42,16 +40,23 @@ export class PathfindingService {
this.sortNodesByDistance(unvisitedNodes);
const closestNode = unvisitedNodes.shift() as Node;
// If we encounter a wall, skip it
if (closestNode.isWall) continue;
if (closestNode.isWall) {
continue;
}
// If the closest node is at an infinite distance, we're trapped
if (closestNode.distance === Infinity) return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
const isTrapped = closestNode.distance === Infinity;
if (isTrapped)
{
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
}
closestNode.isVisited = true;
visitedNodesInOrder.push(closestNode);
if (closestNode === endNode) return { visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode) };
const reachedTheEnd = closestNode === endNode;
if (reachedTheEnd) {
return {visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode)};
}
this.updateUnvisitedNeighbors(closestNode, grid);
}
@@ -75,7 +80,6 @@ export class PathfindingService {
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'];
@@ -83,29 +87,28 @@ export class PathfindingService {
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
}
}
this.initNodesForAStar(allNodes, startNode);
while (openSet.length > 0) {
openSet.sort((nodeA, nodeB) => nodeA['fScore'] - nodeB['fScore']);
const currentNode = openSet.shift() as Node;
if (currentNode.isWall) continue;
if (currentNode.isWall) {
continue;
}
// If the closest node is at an infinite distance, we're trapped
if (currentNode.distance === Infinity) return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
const isTrapped = currentNode.distance === Infinity;
if (isTrapped)
{
return {visitedNodesInOrder, nodesInShortestPathOrder: []};
}
currentNode.isVisited = true;
visitedNodesInOrder.push(currentNode);
if (currentNode === endNode) {
const reachedTheEnd = currentNode === endNode;
if (reachedTheEnd) {
return { visitedNodesInOrder, nodesInShortestPathOrder: this.getNodesInShortestPath(endNode) };
}
@@ -114,14 +117,7 @@ export class PathfindingService {
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);
}
this.updateNeighborNode(neighbor, currentNode, tentativeGScore, endNode, openSet);
}
}
}
@@ -129,6 +125,26 @@ export class PathfindingService {
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
}
private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) {
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);
}
}
private initNodesForAStar(allNodes: Node[], startNode: Node) {
for (const node of allNodes) {
if (node !== startNode) {
node['fScore'] = Infinity;
node.distance = Infinity; // gScore
}
}
}
private calculateHeuristic(node: Node, endNode: Node): number {
// Manhattan distance heuristic
return Math.abs(node.row - endNode.row) + Math.abs(node.col - endNode.col);

View File

@@ -1,25 +1,20 @@
import { Injectable } from '@angular/core';
import { AlgorithmCategory } from '../models/algorithm-category';
import { Observable, of } from 'rxjs';
import {RouterConstants} from '../../../constants/RouterConstants';
@Injectable({
providedIn: 'root'
})
export class AlgorithmsService {
private categories: AlgorithmCategory[] = [
private readonly 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'
// }
title: 'ALGORITHM.PATHFINDING.TITLE',
description: 'ALGORITHM.PATHFINDING.DESCRIPTION',
routerLink: RouterConstants.PATHFINDING.LINK
}
];
getCategories(): Observable<AlgorithmCategory[]> {

View File

@@ -301,14 +301,29 @@
"CLEAR_NODE": "Löschen",
"DIJKSTRA": "Dijkstra",
"ASTAR": "A*",
"RESET_BOARD": "Board zurücksetzten",
"NORMAL_CASE": "Testaufbau",
"EDGE_CASE": "A* Grenzfall",
"CLEAR_BOARD": "Board leeren",
"VISITED": "Besucht",
"PATH": "Pfad",
"PATH_LENGTH": "Pfadlänge",
"EXECUTION_TIME": "Ausführungszeit",
"EXPLANATION": {
"TITLE": "Algorithmen",
"DIJKSTRA_EXPLANATION": " findet garantiert den kürzesten Weg, wenn alle Kantenkosten nicht-negativ sind. Vorteil: optimal und ohne Heuristik. Nachteil: besucht oft sehr viele Knoten (kann bei großen Grids langsamer wirken).",
"ASTAR_EXPLANATION": " erweitert Dijkstra um eine Heuristik (z.B. Manhattan-Distanz) und kann dadurch wesentlich zielgerichteter suchen. Vorteil: oft deutlich schneller bei guter Heuristik; bei zulässiger Heuristik bleibt der Weg optimal. Nachteil: hängt stark von der Heuristik ab (schlechte Heuristik ≈ Dijkstra)."
},
"ALERT": {
"START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten."
}
},
"ALGORITHM": {
"TITLE": "Algorithmen",
"PATHFINDING": {
"TITLE": "Wegfindung",
"DESCRIPTION": "Vergleich von Dijkstra vs. A*.",
"GRID_HEIGHT": "Höhe",
"GRID_WIDTH": "Beite"
}
}
}

View File

@@ -301,14 +301,29 @@
"CLEAR_NODE": "Clear",
"DIJKSTRA": "Dijkstra",
"ASTAR": "A*",
"RESET_BOARD": "Reset Board",
"NORMAL_CASE": "Test Scenario",
"EDGE_CASE": "A* Edge Case",
"CLEAR_BOARD": "Clear Board",
"VISITED": "Visited",
"PATH": "Path",
"PATH_LENGTH": "Path length",
"EXECUTION_TIME": "Execution Time",
"EXPLANATION": {
"TITLE": "Algorithms",
"DIJKSTRA_EXPLANATION": " is guaranteed to find the shortest path if all edge costs are non-negative. Advantage: optimal and without heuristics. Disadvantage: often visits a large number of nodes (can be slower for large grids).",
"ASTAR_EXPLANATION": " extends Dijkstra with a heuristic (e.g. Manhattan distance) and can therefore search in a much more targeted manner. Advantage: often significantly faster with good heuristics; with permissible heuristics, the path remains optimal. Disadvantage: highly dependent on heuristics (poor heuristics ≈ Dijkstra)."
},
"ALERT": {
"START_END_NODES": "Please select a start and end node before running the algorithm."
}
},
"ALGORITHM": {
"TITLE": "Algorithms",
"PATHFINDING": {
"TITLE": "Pathfinding",
"DESCRIPTION": "Comparing of Dijkstra vs. A*.",
"GRID_HEIGHT": "Height",
"GRID_WIDTH": "Width"
}
}
}

View File

@@ -2,8 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import packageJson from '../package.json';
import {AppComponent} from './app/layout/app/app.component';
import { register } from 'swiper/element/bundle';
register();
if (packageJson.version) {

View File

@@ -3,6 +3,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true,
"outDir": "./out-tsc/app",
"types": []
},

View File

@@ -13,7 +13,10 @@
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
"module": "preserve",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,