Merge pull request 'feature/cleanup' (#5) from feature/cleanup into main
All checks were successful
Build & Push Frontend A / docker (push) Successful in 42s
All checks were successful
Build & Push Frontend A / docker (push) Successful in 42s
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
7323
package-lock.json
generated
7323
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -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",
|
||||
|
||||
@@ -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},
|
||||
];
|
||||
|
||||
|
||||
39
src/app/constants/RouterConstants.ts
Normal file
39
src/app/constants/RouterConstants.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<ngx-particles
|
||||
[id]="id"
|
||||
[options]="particlesOptions"
|
||||
[particlesInit]="particlesInit"
|
||||
(particlesLoaded)="particlesLoaded($event)"
|
||||
></ngx-particles>
|
||||
@@ -1,2 +0,0 @@
|
||||
:host { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
||||
.particles { width: 100%; height: 100%; display: block; }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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,58 +20,240 @@ 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.drawGrid();
|
||||
this.ctx = this.getContextOrThrow();
|
||||
this.applyGridSize(true);
|
||||
|
||||
// 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
|
||||
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());
|
||||
}
|
||||
|
||||
initializeGrid(withWalls: boolean): void {
|
||||
this.grid = [];
|
||||
for (let row = 0; row < GRID_ROWS; row++) {
|
||||
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();
|
||||
}
|
||||
|
||||
edgeCase(): void {
|
||||
this.stopAnimations();
|
||||
this.initializeGrid(true, 'edge');
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
clearBoard(): void {
|
||||
this.stopAnimations();
|
||||
this.initializeGrid(false, 'edge');
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
visualizeDijkstra(): void {
|
||||
if (!this.ensureStartAndEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopAnimations();
|
||||
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 < GRID_COLS; col++) {
|
||||
currentRow.push({
|
||||
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,
|
||||
@@ -77,272 +264,51 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 }
|
||||
};
|
||||
}
|
||||
|
||||
stopAnimations(): void {
|
||||
this.timeoutIds.forEach((id) => clearTimeout(id));
|
||||
this.timeoutIds = [];
|
||||
// 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 }
|
||||
};
|
||||
}
|
||||
|
||||
drawGrid(): void {
|
||||
if (!this.ctx) {
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (node.isStart || node.isEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.drawNode(node);
|
||||
}
|
||||
|
||||
visualizeDijkstra(): void {
|
||||
this.stopAnimations();
|
||||
if (!this.startNode || !this.endNode) {
|
||||
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
|
||||
return;
|
||||
}
|
||||
this.clearPath();
|
||||
const startTime = performance.now();
|
||||
const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.dijkstra(this.grid,
|
||||
this.grid[this.startNode.row][this.startNode.col],
|
||||
this.grid[this.endNode.row][this.endNode.col]
|
||||
);
|
||||
const endTime = performance.now();
|
||||
this.pathLength = nodesInShortestPathOrder.length;
|
||||
this.executionTime = endTime - startTime;
|
||||
this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
|
||||
}
|
||||
|
||||
visualizeAStar(): void {
|
||||
this.stopAnimations();
|
||||
if (!this.startNode || !this.endNode) {
|
||||
alert(this.translate.instant('PATHFINDING.ALERT.START_END_NODES'));
|
||||
return;
|
||||
}
|
||||
this.clearPath();
|
||||
const startTime = performance.now();
|
||||
const { visitedNodesInOrder, nodesInShortestPathOrder } = this.pathfindingService.aStar(this.grid,
|
||||
this.grid[this.startNode.row][this.startNode.col],
|
||||
this.grid[this.endNode.row][this.endNode.col]
|
||||
);
|
||||
const endTime = performance.now();
|
||||
this.pathLength = nodesInShortestPathOrder.length;
|
||||
this.executionTime = endTime - startTime;
|
||||
this.animateAlgorithm(visitedNodesInOrder, nodesInShortestPathOrder);
|
||||
}
|
||||
|
||||
animateAlgorithm(visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[]): void {
|
||||
for (let i = 0; i <= visitedNodesInOrder.length; i++) {
|
||||
if (i === visitedNodesInOrder.length) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.animateShortestPath(nodesInShortestPathOrder);
|
||||
}, this.animationSpeed * i);
|
||||
this.timeoutIds.push(timeoutId);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = visitedNodesInOrder[i];
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!node.isStart && !node.isEnd) {
|
||||
node.isVisited = true;
|
||||
this.drawNode(node);
|
||||
}
|
||||
}, this.animationSpeed * i);
|
||||
this.timeoutIds.push(timeoutId);
|
||||
node.isWall = true;
|
||||
}
|
||||
}
|
||||
|
||||
animateShortestPath(nodesInShortestPathOrder: Node[]): void {
|
||||
for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
|
||||
const node = nodesInShortestPathOrder[i];
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!node.isStart && !node.isEnd) {
|
||||
node.isPath = true;
|
||||
this.drawNode(node);
|
||||
}
|
||||
}, this.animationSpeed * i);
|
||||
this.timeoutIds.push(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
drawNode(node: Node): void {
|
||||
if (!this.ctx) return;
|
||||
|
||||
let color = 'lightgray';
|
||||
if (node.isStart) color = 'green';
|
||||
else if (node.isEnd) color = 'red';
|
||||
else if (node.isPath) color = 'gold';
|
||||
else if (node.isVisited) color = 'skyblue';
|
||||
else if (node.isWall) color = 'black';
|
||||
|
||||
this.ctx.fillStyle = color;
|
||||
this.ctx.fillRect(node.col * NODE_SIZE, node.row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
|
||||
this.ctx.strokeStyle = '#ccc';
|
||||
this.ctx.strokeRect(node.col * NODE_SIZE, node.row * NODE_SIZE, NODE_SIZE, NODE_SIZE);
|
||||
}
|
||||
|
||||
resetBoard(): void {
|
||||
this.stopAnimations();
|
||||
this.initializeGrid(true);
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
clearBoard(): void {
|
||||
this.stopAnimations();
|
||||
this.initializeGrid(false);
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
clearPath(): void {
|
||||
this.stopAnimations();
|
||||
for (let row = 0; row < GRID_ROWS; row++) {
|
||||
for (let col = 0; col < GRID_COLS; col++) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,6 +117,15 @@ export class PathfindingService {
|
||||
const tentativeGScore = currentNode.distance + 1; // Distance from start to neighbor
|
||||
|
||||
if (tentativeGScore < neighbor.distance) {
|
||||
this.updateNeighborNode(neighbor, currentNode, tentativeGScore, endNode, openSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -123,10 +135,14 @@ export class PathfindingService {
|
||||
openSet.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
|
||||
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 {
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
|
||||
Reference in New Issue
Block a user