├── .env
├── .gitattributes
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── index.html
└── robots.txt
├── src
├── App.tsx
├── assets
│ ├── react.png
│ ├── start-goal.png
│ ├── weight-tile.png
│ ├── weight.svg
│ └── weight1.svg
├── components
│ ├── Buttons.tsx
│ ├── Checkbox.tsx
│ ├── DraggablePanel.tsx
│ ├── DropDowns.tsx
│ ├── GridBackground.tsx
│ ├── GridForeground.tsx
│ ├── GridVisualization.tsx
│ ├── PathfindingApp.tsx
│ ├── PathfindingVisualizer.tsx
│ ├── RadioButtonGroup.tsx
│ ├── SettingPanels.tsx
│ ├── Stats.tsx
│ ├── SteppedButtonRange.tsx
│ ├── TileFg.tsx
│ ├── Tutorial.tsx
│ └── TutorialPages.tsx
├── index.css
├── index.tsx
├── pathfinding
│ ├── Builders.ts
│ ├── Core.ts
│ ├── Generators.ts
│ ├── Pathfinders.ts
│ └── Structures.ts
├── react-app-env.d.ts
├── styles
│ ├── Grid.scss
│ ├── Navbar.scss
│ ├── Panels.scss
│ ├── Tutorial.scss
│ ├── Utils.scss
│ └── Variables.scss
└── utils
│ ├── AppSettings.ts
│ └── VirtualTimer.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .idea
26 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": true,
5 | "singleQuote": false,
6 | "printWidth": 150,
7 | "bracketSpacing": true
8 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 JosephPrichard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pathfinder
2 |
3 | A website to visualize graph traversal algorithms and maze generation algorithms written using Typescript and React. Contains implementations of A*, Dijkstra, BFS, and DFS with bidirectional variants. Uses the recursive division algorithm to generate mazes with more horizontally or vertically biased variations.
4 |
5 | You can find an online demo [here](https://josephprichard.github.io/pathfinder)!
6 |
7 | ### The Algorithms
8 |
9 | Each algorithm demonstrates different performance/correctness trade-offs.
10 |
11 | `A*` - Guaranteed to always find the shortest path, minimizes number of nodes explored, very high constant time (more suitable for large graphs), uses a Heap for the frontier
12 |
13 | `Dijkstra` - Guaranteed to always find the shortest path, explores many nodes (especially for unweighted graphs), tends to be slower than A*, uses a Heap for the frontier
14 |
15 | `BFS` - Only finds the shortest path for unweighted graphs, explores a reasonable amount of nodes, very low constant time (more suitable for smaller graphs), uses a Queue for the frontier
16 |
17 | `DFS` - Rarely finds the shortest path, explores few nodes, not optimal for pathfinding, uses a stack for the frontier
18 |
19 | `Best First` - Not guaranteed to find the shortest path, explores the least amount of nodes, uses a Heap for the frontier
20 |
21 | ### Deployment
22 |
23 | Add the following property to `package.json` containing the url the web page will be deployed at.
24 | ```
25 | "homepage": "https://josephprichard.github.io/pathfinder"
26 | ```
27 |
28 | Deploy the project to github pages using the following command.
29 | ```
30 | npm run deploy
31 | ```
32 |
33 | ### Pathfinding
34 |
35 | The project also contains standalone object-oriented style abstractions for pathfinding on a 2D grid.
36 |
37 | The grid is based around 3 fundamental interfaces contained in `../pathfinding/core/Components`
38 |
39 | Point, which represents an x,y location on the grid.
40 | TileData, which stores the solidity of a tile and the cost to travel to a tile if it isn't solid.
41 | Tile, which stores Point and TileData, representing a Vertex on the Grid.
42 |
43 | We can create a grid with a width of 10 and a height of 5 like so:
44 | ```typescript
45 | const grid = new Grid(10, 5);
46 | ```
47 |
48 | Let's say we wanted to make the tile at (5,3) solid, so we can't travel there:
49 | ```typescript
50 | const point = {x: 5, y: 3};
51 | const data = {pathCost: 1, isSolid: true};
52 | grid.mutate(point, data);
53 | ```
54 |
55 | If we wanted to get the tile at the point (4,6) we would do it like this:
56 | ```typescript
57 | grid.get({x: 4, y: 6});
58 | ```
59 |
60 | The grid class contains minimal bound checks to speed up processing, but we can check the boundaries on
61 | the grid with these helpful functions:
62 | ```typescript
63 | grid.inBounds(point);
64 | grid.getWidth();
65 | grid.getHeight();
66 | ```
67 |
68 | This project also contains Pathfinders which can find the best path (capable by the algorithm) between an initial and goal point.
69 |
70 | If we want to initialize a pathfinder we need to pass it a navigator.
71 |
72 | A navigator is a class that encapsulates the grid by determining what tiles we can travel to from a given point. The project
73 | contains two build in navigators, but you can make your own as long as they inherit from the abstract Navigator Class in `../pathfinding/core/Navigator`.
74 |
75 | If we wanted to initialize the "PlusNavigator" which allows movement in 4 directions (up,left,right,down) we can do so like:
76 | ```typescript
77 | const navigator = new PlusNavigator(grid);
78 | ```
79 |
80 | We can access the neighbors of the point (3,3) like so:
81 | ```typescript
82 | const neighbors: Tile[] = navigator.neighbors({x: 3, y:3});
83 | ```
84 |
85 | Now we can initialize a pathfinder that uses the AStar algorithm like this:
86 | ```typescript
87 | const pathfinder = new AStarPathfinder(navigator);
88 | ```
89 |
90 | If we wanted to find the shortest path on the grid from (0,0) to (4,3) we would do it like this:
91 | ```typescript
92 | const initial = {x: 0, y: 0};
93 | const goal = {x: 4, y: 3};
94 | const path: Tile[] = pathfinder.findPath(initial, goal);
95 | ```
96 |
97 | The A* algorithm uses the Manhattan distance heuristic by default but you can find other heuristics in `../pathfinding/algorithms/Heuristics`.
98 |
99 | Lastly, we can randomly generate mazes with the TerrainMazeGenerator class:
100 | ```typescript
101 | const generator = new TerrainMazeGenerator(width, height);
102 | ```
103 |
104 | We can invoke the random generation algorithm (recursive division) like:
105 | ```typescript
106 | const maze: Grid = generator.generateTerrain();
107 | ```
108 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "https://josephprichard.github.io/pathfinder",
3 | "name": "pathfinder-react",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@types/node": "^14.17.1",
8 | "@types/react": "^17.0.6",
9 | "@types/react-dom": "^17.0.5",
10 | "react": "^17.0.2",
11 | "react-dom": "^17.0.2",
12 | "react-scripts": "^5.0.1",
13 | "sass": "1.69.5",
14 | "typescript": "^4.2.4"
15 | },
16 | "scripts": {
17 | "format": "prettier --write \"**/*.{ts,tsx}\"",
18 | "predeploy": "npm run build",
19 | "deploy": "gh-pages -d build",
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "run": "serve -s build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
45 | "gh-pages": "^3.2.0",
46 | "prettier": "^3.3.3",
47 | "sass-loader": "^10.1.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
14 |
15 | Pathfinding Visualizer
16 |
17 |
18 | You need to enable JavaScript to run this app.
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 | import "./styles/Grid.scss";
7 | import "./styles/Panels.scss";
8 | import "./styles/Navbar.scss";
9 | import "./styles/Tutorial.scss";
10 | import PathfindingApp from "./components/PathfindingApp";
11 |
12 | class App extends React.Component {
13 | render() {
14 | return ;
15 | }
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/assets/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosephPrichard/pathfinder/b9d85355f22586235bf0c1b8628bff4ed465e480/src/assets/react.png
--------------------------------------------------------------------------------
/src/assets/start-goal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosephPrichard/pathfinder/b9d85355f22586235bf0c1b8628bff4ed465e480/src/assets/start-goal.png
--------------------------------------------------------------------------------
/src/assets/weight-tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JosephPrichard/pathfinder/b9d85355f22586235bf0c1b8628bff4ed465e480/src/assets/weight-tile.png
--------------------------------------------------------------------------------
/src/assets/weight.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/assets/weight1.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
10 | Svg Vector Icons : http://www.onlinewebfonts.com/icon
11 |
12 |
--------------------------------------------------------------------------------
/src/components/Buttons.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 |
7 | interface VizButtonProps {
8 | active: boolean;
9 | paused: boolean;
10 | onStartStop: () => void;
11 | onPause: () => void;
12 | onResume: () => void;
13 | }
14 |
15 | interface ButtonProps {
16 | onClick: () => void;
17 | }
18 |
19 | const SYMBOL_COLOR = "rgb(230,230,230)";
20 | const OFFSET = 14;
21 | const DIMENSION = 47 - 2 * OFFSET;
22 |
23 | export function VisualizeButton(props: VizButtonProps) {
24 | const getStopSymbol = () => ;
25 |
26 | const getResumeSymbol = () => {
27 | const midY = DIMENSION / 2;
28 | return ;
29 | };
30 |
31 | const getPauseSymbol = () => (
32 |
33 |
34 |
35 |
36 | );
37 |
38 | return props.active ? (
39 |
40 | e.preventDefault()}
42 | className={"center half-button-left red-button half-viz-button"}
43 | onClick={props.paused ? props.onResume : props.onPause}
44 | >
45 |
46 | {props.paused ? getResumeSymbol() : getPauseSymbol()}
47 |
48 |
49 | e.preventDefault()}
51 | className={"center half-button-right red-button half-viz-button"}
52 | onClick={props.onStartStop}
53 | >
54 |
55 | {getStopSymbol()}
56 |
57 |
58 |
59 | ) : (
60 | e.preventDefault()} className={"button green-button viz-button"} onClick={props.onStartStop}>
61 | Visualize!
62 |
63 | );
64 | }
65 |
66 | export function SettingsButton({ onClick }: ButtonProps) {
67 | return (
68 | e.preventDefault()} className="special-button" onClick={onClick}>
69 | Settings
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 |
7 | interface Props {
8 | boxStyle: string;
9 | defaultChecked: boolean;
10 | disabled?: boolean;
11 | onChange: (checked: boolean) => void;
12 | }
13 |
14 | interface State {
15 | checked: boolean;
16 | }
17 |
18 | class Checkbox extends React.Component {
19 | public static defaultProps = {
20 | disabled: false,
21 | };
22 |
23 | constructor(props: Props) {
24 | super(props);
25 | this.state = {
26 | checked: this.props.defaultChecked,
27 | };
28 | }
29 |
30 | onChange() {
31 | this.setState(
32 | (prevState) => ({ checked: !prevState.checked }),
33 | () => this.props.onChange(this.state.checked)
34 | );
35 | }
36 |
37 | render() {
38 | return (
39 |
40 | this.onChange()}
46 | onChange={() => this.onChange()}
47 | />
48 | {this.props.children}
49 |
50 | );
51 | }
52 | }
53 |
54 | export default Checkbox;
55 |
--------------------------------------------------------------------------------
/src/components/DraggablePanel.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React, { RefObject } from "react";
6 |
7 | interface Props {
8 | title: string;
9 | show: boolean;
10 | onClickXButton: () => void;
11 | width: number;
12 | height: number;
13 | }
14 |
15 | interface State {
16 | top: number;
17 | left: number;
18 | }
19 |
20 | class DraggablePanel extends React.Component {
21 | // refs are used to access native DOM
22 | private draggable: RefObject = React.createRef();
23 | private draggableContainer: RefObject = React.createRef();
24 | private draggableContent: RefObject = React.createRef();
25 |
26 | // stores previous mouse location and drag
27 | private dragging = false;
28 | private prevX = 0;
29 | private prevY = 0;
30 |
31 | constructor(props: Props) {
32 | super(props);
33 | this.state = { top: -1, left: -1 };
34 | }
35 |
36 | componentDidMount() {
37 | // mouse
38 | document.addEventListener("mouseup", this.mouseUp);
39 | document.addEventListener("mousemove", this.mouseMove);
40 | window.addEventListener("mouseleave", this.mouseUp);
41 | // touch
42 | document.addEventListener("touchend", this.stopDrag);
43 | document.addEventListener("touchmove", this.touchMove);
44 | }
45 |
46 | componentWillUnmount() {
47 | // mouse
48 | document.removeEventListener("mouseup", this.mouseUp);
49 | document.removeEventListener("mousemove", this.mouseMove);
50 | window.removeEventListener("mouseleave", this.mouseUp);
51 | // touch
52 | document.removeEventListener("touchend", this.stopDrag);
53 | document.removeEventListener("touchmove", this.touchMove);
54 | }
55 |
56 | stopDrag = () => {
57 | this.dragging = false;
58 | };
59 |
60 | mouseDown = (e: MouseEvent) => {
61 | e.preventDefault();
62 | this.prevY = e.clientY;
63 | this.prevX = e.clientX;
64 | this.dragging = true;
65 | };
66 |
67 | touchStart = (e: TouchEvent) => {
68 | const touch = e.touches[0] || e.changedTouches[0];
69 | this.prevY = touch.clientY;
70 | this.prevX = touch.clientX;
71 | this.dragging = true;
72 | };
73 |
74 | mouseUp = (e: Event) => {
75 | e.preventDefault();
76 | this.dragging = false;
77 | };
78 |
79 | mouseMove = (e: MouseEvent) => {
80 | this.drag(e.clientX, e.clientY);
81 | };
82 |
83 | touchMove = (e: TouchEvent) => {
84 | const touch = e.touches[0] || e.changedTouches[0];
85 | this.drag(touch.clientX, touch.clientY);
86 | };
87 |
88 | drag(clientX: number, clientY: number) {
89 | if (this.dragging) {
90 | const container = this.draggableContainer.current!;
91 | let top = container.offsetTop - (this.prevY - clientY);
92 | let left = container.offsetLeft - (this.prevX - clientX);
93 | const content = this.draggableContent.current!;
94 | const draggable = this.draggable.current!;
95 | // stop drag if mouse goes out of bounds
96 | if (clientY < 0 || clientY > window.innerHeight || clientX < 0 || clientX > window.innerWidth) {
97 | this.dragging = false;
98 | }
99 | // check if position is out of bounds and prevent the panel from being dragged there
100 | if (top < 0) {
101 | top = 0;
102 | } else if (top > window.innerHeight - draggable.offsetHeight) {
103 | top = window.innerHeight - draggable.offsetHeight;
104 | }
105 | if (left < -content.offsetWidth / 2) {
106 | left = -content.offsetWidth / 2;
107 | } else if (left > window.innerWidth - content.offsetWidth / 2) {
108 | left = window.innerWidth - content.offsetWidth / 2;
109 | }
110 | // set new position
111 | this.setState({ top, left });
112 | // update previous pos
113 | this.prevY = clientY;
114 | this.prevX = clientX;
115 | }
116 | }
117 |
118 | getPosition() {
119 | const left = this.state.left;
120 | const top = this.state.top;
121 | if (left === -1 || top === -1) {
122 | return {};
123 | }
124 | return { left: left + "px", top: top + "px" };
125 | }
126 |
127 | visibleStyle() {
128 | return this.props.show ? "block" : "none";
129 | }
130 |
131 | draggableStyle() {
132 | return {
133 | width: this.props.width,
134 | display: this.visibleStyle(),
135 | };
136 | }
137 |
138 | contentStyle() {
139 | return {
140 | width: this.props.width,
141 | minHeight: this.props.height,
142 | display: this.visibleStyle(),
143 | };
144 | }
145 |
146 | render() {
147 | return (
148 |
149 | {this.renderDraggable()}
150 |
151 |
{this.props.children}
152 |
153 |
154 | );
155 | }
156 |
157 | renderDraggable() {
158 | return (
159 | this.mouseDown(e.nativeEvent)}
164 | onTouchStart={(e) => this.touchStart(e.nativeEvent)}
165 | >
166 |
{this.props.title}
167 |
{
173 | e.stopPropagation();
174 | e.preventDefault();
175 | }}
176 | >
177 |
X
178 |
179 |
180 | );
181 | }
182 | }
183 |
184 | export default DraggablePanel;
185 |
--------------------------------------------------------------------------------
/src/components/DropDowns.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React, { RefObject } from "react";
6 |
7 | export interface DropDownProps {
8 | onClick: () => void;
9 | text: string;
10 | dropDownClass?: string;
11 | dropDownContentClass?: string;
12 | }
13 |
14 | export interface DropDownState {
15 | up: boolean;
16 | display: string;
17 | fade: string;
18 | }
19 |
20 | interface DropDownTextState {
21 | text: string;
22 | }
23 |
24 | interface AlgProps {
25 | onClick: () => void;
26 | onChange: (alg: string) => void;
27 | }
28 |
29 | interface ClrProps {
30 | onClick: () => void;
31 | onClickPath: () => void;
32 | onClickTiles: () => void;
33 | onClickReset: () => void;
34 | }
35 |
36 | interface MazeProps {
37 | onClick: () => void;
38 | onClickMaze: () => void;
39 | onClickMazeHorizontal: () => void;
40 | onClickMazeVertical: () => void;
41 | onClickRandomTerrain: () => void;
42 | }
43 |
44 | interface TileProps {
45 | onClick: () => void;
46 | onClickTileType: (cost: number) => void;
47 | }
48 |
49 | interface ClickableProps {
50 | click: () => void;
51 | }
52 |
53 | class Clickable extends React.Component {
54 | render() {
55 | return (
56 |
57 | {this.props.children}
58 |
59 | );
60 | }
61 | }
62 |
63 | class DropDown extends React.Component {
64 | protected constructor(props: DropDownProps) {
65 | super(props);
66 | this.state = {
67 | up: true,
68 | display: "none",
69 | fade: "fade-in",
70 | };
71 | }
72 |
73 | windowOnClick = () => {
74 | this.hide();
75 | };
76 |
77 | componentDidMount() {
78 | window.addEventListener("click", this.windowOnClick);
79 | }
80 |
81 | componentWillUnmount() {
82 | window.removeEventListener("click", this.windowOnClick);
83 | }
84 |
85 | show() {
86 | this.setState({
87 | up: false,
88 | display: "block",
89 | });
90 | }
91 |
92 | hide() {
93 | this.setState({
94 | display: "none",
95 | up: true,
96 | });
97 | }
98 |
99 | toggle(e: Event) {
100 | e.stopPropagation();
101 | this.props.onClick();
102 | if (this.isHidden()) {
103 | this.show();
104 | } else {
105 | this.hide();
106 | }
107 | }
108 |
109 | isHidden() {
110 | return this.state.display === "none";
111 | }
112 |
113 | contentStyle() {
114 | return {
115 | display: this.state.display,
116 | };
117 | }
118 |
119 | arrowClass() {
120 | return this.state.up ? "arrowUp" : "arrowDown";
121 | }
122 |
123 | getHighlightClass() {
124 | return !this.state.up ? "drop-down-button-down drop-down-button-up" : "drop-down-button-up";
125 | }
126 |
127 | render() {
128 | const className = this.props.dropDownClass === undefined ? "" : this.props.dropDownClass;
129 | const contentClassName = this.props.dropDownContentClass === undefined ? "" : this.props.dropDownContentClass;
130 | return (
131 | e.preventDefault()}
135 | onKeyPress={(e) => this.toggle(e.nativeEvent)}
136 | onClick={(e) => this.toggle(e.nativeEvent)}
137 | >
138 |
139 |
140 | {this.props.text}
141 |
142 |
143 |
144 |
145 | {this.props.children}
146 |
147 |
148 | );
149 | }
150 | }
151 |
152 | export class AlgorithmDropDown extends React.Component {
153 | private dropDown: RefObject = React.createRef();
154 |
155 | constructor(props: AlgProps) {
156 | super(props);
157 | this.state = { text: "A* Search" };
158 | }
159 |
160 | hide() {
161 | this.dropDown.current!.hide();
162 | }
163 |
164 | onChange(key: string, algText: string) {
165 | this.props.onChange(key);
166 | this.setState({ text: algText });
167 | }
168 |
169 | render() {
170 | return (
171 |
172 | this.onChange("a*", "A* Search")}>A* Search
173 | this.onChange("dijkstra", "Dijkstra")}>Dijkstra's Algorithm
174 | this.onChange("best-first", "Best First")}>Best First Search
175 | this.onChange("bfs", "Breadth First")}>Breadth First Search
176 | this.onChange("dfs", "Depth First")}>Depth First Search
177 |
178 | );
179 | }
180 | }
181 |
182 | export class ClearDropDown extends React.Component {
183 | private dropDown: RefObject = React.createRef();
184 |
185 | hide() {
186 | this.dropDown.current!.hide();
187 | }
188 |
189 | render() {
190 | return (
191 |
192 | Clear Path
193 | Clear Tiles
194 | Reset Grid
195 |
196 | );
197 | }
198 | }
199 |
200 | export class MazeDropDown extends React.Component {
201 | private dropDown: RefObject = React.createRef();
202 |
203 | hide() {
204 | this.dropDown.current!.hide();
205 | }
206 |
207 | render() {
208 | return (
209 |
216 | Recursive Maze Division
217 | Horizontal Skewed Maze
218 | Vertical Skewed Maze
219 | Random Terrain
220 |
221 | );
222 | }
223 | }
224 |
225 | export class TilesDropDown extends React.Component {
226 | private dropDown: RefObject = React.createRef();
227 |
228 | constructor(props: TileProps) {
229 | super(props);
230 | this.state = { text: "Wall [∞]" };
231 | }
232 |
233 | hide() {
234 | this.dropDown.current!.hide();
235 | }
236 |
237 | onChange(cost: number, text: string) {
238 | this.props.onClickTileType(cost);
239 | this.setState({ text: text }, () => this.props.onClickTileType(cost));
240 | }
241 |
242 | render() {
243 | return (
244 |
251 | this.onChange(-1, "Wall [∞]")}>Wall [∞]
252 | this.onChange(2, "Weight [2]")}>Weight [2]
253 | this.onChange(3, "Weight [3]")}>Weight [3]
254 | this.onChange(5, "Weight [5]")}>Weight [5]
255 |
256 | );
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/src/components/GridBackground.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 | import { Point } from "../pathfinding/Core";
7 |
8 | interface Props {
9 | tileWidth: number;
10 | width: number;
11 | height: number;
12 | }
13 |
14 | class GridBackground extends React.Component {
15 | private readonly tileWidth: number;
16 |
17 | constructor(props: Props) {
18 | super(props);
19 | this.tileWidth = this.props.tileWidth;
20 | }
21 |
22 | componentDidUpdate(prevProps: Props) {
23 | return this.props.width !== prevProps.width || this.props.height !== prevProps.height;
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
{this.renderTiles()}
30 |
31 | );
32 | }
33 |
34 | renderTiles() {
35 | const tiles: JSX.Element[][] = [];
36 | for (let y = 0; y < this.props.height; y++) {
37 | const row: JSX.Element[] = [];
38 | for (let x = 0; x < this.props.width; x++) {
39 | const point = { x, y };
40 | row.push(this.renderTile(point));
41 | }
42 | tiles.push(row);
43 | }
44 | return tiles;
45 | }
46 |
47 | renderTile(point: Point) {
48 | const width = this.tileWidth;
49 | const top = point.y * this.tileWidth;
50 | const left = point.x * this.tileWidth;
51 | const style = {
52 | backgroundColor: "white",
53 | width: width + "px",
54 | height: width + "px",
55 | top: top,
56 | left: left,
57 | };
58 | return
;
59 | }
60 | }
61 |
62 | export default GridBackground;
63 |
--------------------------------------------------------------------------------
/src/components/GridForeground.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React, { RefObject } from "react";
6 | import { TileFg, SolidFg, WeightFg } from "./TileFg";
7 | import { createTileData, Grid, Point, RectGrid, Tile, TileData } from "../pathfinding/Core";
8 |
9 | interface Props {
10 | tileSize: number;
11 | width: number;
12 | height: number;
13 | onTilesDragged: () => void;
14 | end: Point;
15 | }
16 |
17 | interface State {
18 | grid: Grid;
19 | initial: Point;
20 | goal: Point;
21 | path: Tile[];
22 | }
23 |
24 | const INITIAL_COLOR = "rgb(131, 217, 52)";
25 | const GOAL_COLOR = "rgb(203, 75, 14)";
26 | const ARROW_PATH_COLOR = "rgb(73, 79, 250)";
27 |
28 | const BASE_WIDTH = 27;
29 |
30 | class GridForeground extends React.Component {
31 | private svg: RefObject = React.createRef();
32 |
33 | private tilePointer: TileData;
34 |
35 | private drawing: boolean = false;
36 | private erasing: boolean = false;
37 | private draggingInitial: boolean = false;
38 | private draggingGoal: boolean = false;
39 | private disable: boolean = false;
40 |
41 | private doTileAnimation: boolean = true;
42 |
43 | private initialKey: number = 0;
44 | private goalKey: number = 0;
45 |
46 | constructor(props: Props) {
47 | super(props);
48 | const end = this.props.end;
49 | this.tilePointer = createTileData(true);
50 | this.state = {
51 | grid: new RectGrid(this.props.width, this.props.height),
52 | path: [],
53 | initial: {
54 | x: (end.x / 3) >> 0,
55 | y: (end.y / 3) >> 0,
56 | },
57 | goal: {
58 | x: (((2 * end.x) / 3) >> 0) - 1,
59 | y: (((2 * end.y) / 3) >> 0) - 1,
60 | },
61 | };
62 | }
63 |
64 | componentDidUpdate(prevProps: Props) {
65 | if (this.props.width !== prevProps.width || this.props.height !== prevProps.height) {
66 | this.setState((prevState) => ({
67 | grid: prevState.grid.cloneNewSize(this.props.width, this.props.height),
68 | }));
69 | }
70 | }
71 |
72 | changeTile(data: TileData) {
73 | this.tilePointer = data;
74 | }
75 |
76 | toggleDisable() {
77 | this.disable = !this.disable;
78 | }
79 |
80 | getBoundingRect() {
81 | return this.svg.current!.getBoundingClientRect();
82 | }
83 |
84 | mouseDown(e: MouseEvent) {
85 | e.preventDefault();
86 | const bounds = this.getBoundingRect();
87 | this.onPress(e.clientX - bounds.left, e.clientY - bounds.top, e.button);
88 | }
89 |
90 | mouseUp(e: MouseEvent) {
91 | e.preventDefault();
92 | if (isControlKey(e.button)) {
93 | this.draggingGoal = false;
94 | this.draggingInitial = false;
95 | this.drawing = false;
96 | this.erasing = false;
97 | }
98 | }
99 |
100 | mouseMove(e: MouseEvent) {
101 | const bounds = this.getBoundingRect();
102 | this.onDrag(e.clientX - bounds.left, e.clientY - bounds.top);
103 | }
104 |
105 | touchStart(e: TouchEvent) {
106 | const touch = e.touches[0] || e.changedTouches[0];
107 | const bounds = this.getBoundingRect();
108 | this.onPress(touch.clientX - bounds.left, touch.clientY - bounds.top, 0);
109 | }
110 |
111 | touchMove(e: TouchEvent) {
112 | const touch = e.touches[0] || e.changedTouches[0];
113 | const bounds = this.getBoundingRect();
114 | this.onDrag(touch.clientX - bounds.left, touch.clientY - bounds.top);
115 | }
116 |
117 | onEndingEvent(e: Event) {
118 | e.preventDefault();
119 | this.draggingGoal = false;
120 | this.draggingInitial = false;
121 | this.drawing = false;
122 | this.erasing = false;
123 | }
124 |
125 | onPress(xCoordinate: number, yCoordinate: number, button: number) {
126 | const point = this.calculatePoint(xCoordinate, yCoordinate);
127 | if (isControlKey(button)) {
128 | if (pointsEqual(point, this.state.initial)) {
129 | this.draggingInitial = true;
130 | } else if (pointsEqual(point, this.state.goal)) {
131 | this.draggingGoal = true;
132 | } else if (!this.disable) {
133 | if (this.state.grid.isEmpty(point)) {
134 | this.drawing = true;
135 | this.drawTile(point);
136 | } else {
137 | this.erasing = true;
138 | this.eraseTile(point);
139 | }
140 | }
141 | }
142 | }
143 |
144 | onDrag(xCoordinate: number, yCoordinate: number) {
145 | const point = this.calculatePoint(xCoordinate, yCoordinate);
146 | if (this.draggingInitial) {
147 | this.moveInitial(point);
148 | } else if (this.draggingGoal) {
149 | this.moveGoal(point);
150 | } else if (!pointsEqual(point, this.state.initial) && !pointsEqual(point, this.state.goal) && !this.disable) {
151 | if (this.drawing) {
152 | this.drawTile(point);
153 | } else if (this.erasing) {
154 | this.eraseTile(point);
155 | }
156 | }
157 | }
158 |
159 | drawGrid(grid: Grid) {
160 | this.doTileAnimation = false;
161 | this.setState({ grid }, () => (this.doTileAnimation = true));
162 | }
163 |
164 | drawTile(point: Point) {
165 | const grid = this.state.grid.clone();
166 | if (grid.inBounds(point)) {
167 | grid.mutateTile({ point: point, data: this.tilePointer });
168 | }
169 | this.setState({ grid: grid });
170 | }
171 |
172 | eraseTile(point: Point) {
173 | const grid = this.state.grid.clone();
174 | if (grid.inBounds(point)) {
175 | grid.mutateDefault(point, false);
176 | }
177 | this.setState({ grid: grid });
178 | }
179 |
180 | clearTiles() {
181 | const grid = this.state.grid.clone();
182 | for (let y = 0; y < this.state.grid.getHeight(); y++) {
183 | for (let x = 0; x < this.state.grid.getWidth(); x++) {
184 | grid.mutateDefault({ x, y }, false);
185 | }
186 | }
187 | this.setState({ grid: grid });
188 | }
189 |
190 | moveInitial(point: Point) {
191 | if (this.canMoveEndPoint(point)) {
192 | this.initialKey++;
193 | this.setState({ initial: point }, () => this.props.onTilesDragged());
194 | }
195 | }
196 |
197 | moveGoal(point: Point) {
198 | if (this.canMoveEndPoint(point)) {
199 | this.goalKey++;
200 | this.setState({ goal: point }, () => this.props.onTilesDragged());
201 | }
202 | }
203 |
204 | canMoveEndPoint(point: Point) {
205 | return (
206 | this.state.grid.inBounds(point) &&
207 | this.state.grid.isEmpty(point) &&
208 | !pointsEqual(this.state.initial, point) &&
209 | !pointsEqual(this.state.goal, point) &&
210 | !this.disable
211 | );
212 | }
213 |
214 | drawPath(path: Tile[]) {
215 | this.setState({ path: path.slice() });
216 | }
217 |
218 | erasePath() {
219 | this.setState({ path: [] });
220 | }
221 |
222 | calculatePoint(xCoordinate: number, yCoordinate: number) {
223 | return {
224 | x: Math.floor(xCoordinate / this.props.tileSize),
225 | y: Math.floor(yCoordinate / this.props.tileSize),
226 | };
227 | }
228 |
229 | resetPoints() {
230 | this.initialKey++;
231 | this.goalKey++;
232 | const end = this.props.end;
233 | this.setState({
234 | initial: {
235 | x: (end.x / 3) >> 0,
236 | y: (end.y / 3) >> 0,
237 | },
238 | goal: {
239 | x: (((2 * end.x) / 3) >> 0) - 1,
240 | y: (((2 * end.y) / 3) >> 0) - 1,
241 | },
242 | });
243 | }
244 |
245 | render() {
246 | return (
247 |
248 |
249 | {this.renderEndTile(this.state.initial, INITIAL_COLOR, "initial" + this.initialKey)}
250 | {this.renderEndTile(this.state.goal, GOAL_COLOR, "goal" + this.goalKey)}
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 | {this.renderPath()}
259 |
260 |
e.preventDefault()}
263 | onMouseDown={(e) => this.mouseDown(e.nativeEvent)}
264 | onMouseUp={(e) => this.mouseUp(e.nativeEvent)}
265 | onMouseMove={(e) => this.mouseMove(e.nativeEvent)}
266 | onMouseLeave={(e) => this.onEndingEvent(e.nativeEvent)}
267 | onTouchStart={(e) => this.touchStart(e.nativeEvent)}
268 | onTouchMoveCapture={(e) => this.touchMove(e.nativeEvent)}
269 | onTouchEnd={(e) => this.onEndingEvent(e.nativeEvent)}
270 | onTouchCancel={(e) => this.onEndingEvent(e.nativeEvent)}
271 | >
272 | {this.renderTilesGrid()}
273 |
274 |
275 | );
276 | }
277 |
278 | renderPath() {
279 | const lines: JSX.Element[] = [];
280 | for (let i = 0; i < this.state.path.length - 1; i++) {
281 | const first = this.state.path[i].point;
282 | const second = this.state.path[i + 1].point;
283 | lines.push(this.renderPathArrow(i, first, second));
284 | }
285 | return lines;
286 | }
287 |
288 | renderPathArrow(index: number, first: Point, second: Point) {
289 | const width = this.props.tileSize;
290 | const offset = width / 2;
291 | const firstX = first.x * width;
292 | const firstY = first.y * width;
293 | const secondX = second.x * width;
294 | const secondY = second.y * width;
295 | const offsetX = (secondX - firstX) / 4;
296 | const offsetY = (secondY - firstY) / 4;
297 | return (
298 |
309 | );
310 | }
311 |
312 | renderTilesGrid() {
313 | const tiles: JSX.Element[] = [];
314 | for (let y = 0; y < this.state.grid.getHeight(); y++) {
315 | for (let x = 0; x < this.state.grid.getWidth(); x++) {
316 | const point = { x, y };
317 | const cost = this.state.grid.get(point).data.pathCost;
318 |
319 | if (this.state.grid.isSolid(point)) {
320 | tiles.push( );
321 | } else if (cost > 1) {
322 | tiles.push( );
323 | tiles.push(this.renderWeightText(point, cost, x + "," + y + " text"));
324 | }
325 | }
326 | }
327 | return tiles;
328 | }
329 |
330 | renderWeightText(point: Point, cost: number, key: string) {
331 | return (
332 |
347 | {cost}
348 |
349 | );
350 | }
351 |
352 | renderEndTile(point: Point, color: string, key: string) {
353 | return ;
354 | }
355 | }
356 |
357 | function pointsEqual(point1: Point, point2: Point) {
358 | return point1.x === point2.x && point1.y === point2.y;
359 | }
360 |
361 | function isControlKey(button: number) {
362 | // right or left mouse
363 | return button === 0 || button === 2;
364 | }
365 |
366 | export default GridForeground;
367 |
--------------------------------------------------------------------------------
/src/components/GridVisualization.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 | import AppSettings from "../utils/AppSettings";
7 | import { PointTable } from "../pathfinding/Structures";
8 | import { Point } from "../pathfinding/Core";
9 | import { PathNode } from "../pathfinding/Pathfinders";
10 |
11 | const CLOSED_NODE = "rgb(198, 237, 238)";
12 | const OPEN_NODE = "rgb(191, 248, 159)";
13 | const ARROW_COLOR = "rgb(153,153,153)";
14 | const EMPTY_NODE = "e";
15 | const TILE_CLASS = "tile";
16 | const VIZ_TILE_CLASS = "tile tile-viz";
17 |
18 | const BASE_WIDTH = 27;
19 |
20 | interface Arrow {
21 | to: Point;
22 | from: Point;
23 | }
24 |
25 | interface Props {
26 | settings: AppSettings;
27 | tileWidth: number;
28 | width: number;
29 | height: number;
30 | }
31 |
32 | // scores and visualization are parallel arrays
33 | interface State {
34 | visualization: string[][];
35 | arrows: PointTable; // arrows are uniquely defined by where they point to
36 | }
37 |
38 | function clone(array: T[][]) {
39 | return array.map((arr) => arr.slice());
40 | }
41 |
42 | class GridVisualization extends React.Component {
43 | private readonly tileWidth: number;
44 | private tileClass: string = TILE_CLASS;
45 | private readonly width: number;
46 | private readonly height: number;
47 |
48 | constructor(props: Props) {
49 | super(props);
50 | this.tileWidth = this.props.tileWidth;
51 | this.width = props.width;
52 | this.height = props.height;
53 | this.state = {
54 | visualization: this.createEmptyViz(),
55 | arrows: new PointTable(props.width, props.height),
56 | };
57 | }
58 |
59 | static doVizGeneration(generation: PathNode, visualization: string[][]) {
60 | for (const node of generation.children) {
61 | const point = node.tile.point;
62 | visualization[point.y][point.x] = OPEN_NODE;
63 | }
64 | const point = generation.tile.point;
65 | visualization[point.y][point.x] = CLOSED_NODE;
66 | return visualization;
67 | }
68 |
69 | static doArrowGeneration(generation: PathNode, arrows: PointTable) {
70 | const point = generation.tile.point;
71 | for (const node of generation.children) {
72 | const childPoint = node.tile.point;
73 | const newArrow = {
74 | from: point,
75 | to: childPoint,
76 | };
77 | // remove a duplicate arrow to indicate replacement
78 | // in A* for example, we could have re-discovered a better path to a tile
79 | arrows.add(newArrow.to, newArrow);
80 | }
81 | return arrows;
82 | }
83 |
84 | componentDidUpdate(prevProps: Readonly) {
85 | if (this.props.width !== prevProps.width || this.props.height !== prevProps.height) {
86 | const visualization: string[][] = this.createEmptyViz();
87 | for (let y = 0; y < this.props.height; y++) {
88 | for (let x = 0; x < this.props.width; x++) {
89 | if (y < prevProps.height && x < prevProps.width) {
90 | visualization[y][x] = this.state.visualization[y][x];
91 | }
92 | }
93 | }
94 | this.setState({ visualization });
95 | }
96 | }
97 |
98 | createEmptyViz() {
99 | const visualization: string[][] = [];
100 | for (let y = 0; y < this.props.height; y++) {
101 | const row: string[] = [];
102 | for (let x = 0; x < this.props.width; x++) {
103 | row.push(EMPTY_NODE);
104 | }
105 | visualization.push(row);
106 | }
107 | return visualization;
108 | }
109 |
110 | clear() {
111 | this.setState({
112 | visualization: this.createEmptyViz(),
113 | arrows: new PointTable(this.width, this.height),
114 | });
115 | }
116 |
117 | visualizeGeneration(generation: PathNode) {
118 | this.setState((prevState) => ({
119 | visualization: GridVisualization.doVizGeneration(generation, clone(prevState.visualization)),
120 | }));
121 | }
122 |
123 | enableAnimations() {
124 | this.tileClass = VIZ_TILE_CLASS;
125 | }
126 |
127 | disableAnimations() {
128 | this.tileClass = TILE_CLASS;
129 | }
130 |
131 | visualizeGenerations(generations: PathNode[]) {
132 | const visualization = this.createEmptyViz();
133 | for (const generation of generations) {
134 | GridVisualization.doVizGeneration(generation, visualization);
135 | }
136 | this.setState({ visualization });
137 | }
138 |
139 | addArrowGeneration(generation: PathNode) {
140 | this.setState((prevState) => ({
141 | arrows: GridVisualization.doArrowGeneration(generation, prevState.arrows.clone()),
142 | }));
143 | }
144 |
145 | addArrowGenerations(generations: PathNode[]) {
146 | const arrows: PointTable = new PointTable(this.width, this.height);
147 | for (const generation of generations) {
148 | GridVisualization.doArrowGeneration(generation, arrows);
149 | }
150 | this.setState({ arrows });
151 | }
152 |
153 | visualizeGenerationAndArrows(generation: PathNode) {
154 | this.setState((prevState) => ({
155 | visualization: GridVisualization.doVizGeneration(generation, clone(prevState.visualization)),
156 | arrows: GridVisualization.doArrowGeneration(generation, prevState.arrows.clone()),
157 | }));
158 | }
159 |
160 | render() {
161 | return (
162 |
163 |
{this.renderViz()}
164 |
165 |
166 |
167 |
168 |
169 |
170 | {this.props.settings.showArrows ? this.renderArrows() : []}
171 |
172 |
173 | );
174 | }
175 |
176 | renderArrows() {
177 | const width = this.tileWidth;
178 | const offset = width / 2;
179 | const arrows: JSX.Element[] = [];
180 | const arrowList = this.state.arrows.values();
181 |
182 | for (let i = 0; i < arrowList.length; i++) {
183 | const arrow = arrowList[i];
184 | const first = arrow.from;
185 | const second = arrow.to;
186 | const firstX = first.x * width;
187 | const firstY = first.y * width;
188 | const secondX = second.x * width;
189 | const secondY = second.y * width;
190 | const offsetX = (secondX - firstX) / 4;
191 | const offsetY = (secondY - firstY) / 4;
192 |
193 | arrows.push(
194 |
205 | );
206 | }
207 | return arrows;
208 | }
209 |
210 | renderViz() {
211 | const tiles: JSX.Element[][] = [];
212 | for (let y = 0; y < this.props.height; y++) {
213 | const row: JSX.Element[] = [];
214 | for (let x = 0; x < this.props.width; x++) {
215 | const inBounds = (this.state.visualization[y] || [])[x] !== undefined;
216 | const viz = inBounds ? this.state.visualization[y][x] : EMPTY_NODE;
217 | if (viz !== EMPTY_NODE) {
218 | const point = { x: x, y: y };
219 | row.push(this.renderTile(point, viz));
220 | }
221 | }
222 | tiles.push(row);
223 | }
224 | return tiles;
225 | }
226 |
227 | renderTile(point: Point, color: string) {
228 | const width = this.tileWidth;
229 | const top = point.y * width;
230 | const left = point.x * width;
231 | const style = {
232 | backgroundColor: color,
233 | width: width + "px",
234 | height: width + "px",
235 | top: top,
236 | left: left,
237 | fontSize: (10 * width) / BASE_WIDTH,
238 | };
239 | return
;
240 | }
241 | }
242 |
243 | export default GridVisualization;
244 |
--------------------------------------------------------------------------------
/src/components/PathfindingApp.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React, { PropsWithChildren, RefObject } from "react";
6 | import { SettingsButton, VisualizeButton } from "./Buttons";
7 | import { AlgorithmDropDown, ClearDropDown, MazeDropDown, TilesDropDown } from "./DropDowns";
8 | import { AlgorithmSettings, HeuristicSettings, SpeedSettings, VisualSettings } from "./SettingPanels";
9 | import DraggablePanel from "./DraggablePanel";
10 | import PathfindingVisualizer from "./PathfindingVisualizer";
11 | import Icon from "../assets/react.png";
12 | import AppSettings, { getDefaultSettings } from "../utils/AppSettings";
13 | import Tutorial, { KEY_SHOW } from "./Tutorial";
14 | import { getTutorialPages } from "./TutorialPages";
15 | import { MAZE, MAZE_HORIZONTAL_SKEW, MAZE_VERTICAL_SKEW, PathfinderBuilder, RANDOM_TERRAIN } from "../pathfinding/Builders";
16 |
17 | interface Props {}
18 |
19 | interface State {
20 | settings: AppSettings;
21 |
22 | heuristicDisabled: boolean;
23 | bidirectionalDisabled: boolean;
24 | arrowsDisabled: boolean;
25 | scoreDisabled: boolean;
26 |
27 | panelShow: boolean;
28 |
29 | visualizing: boolean;
30 | paused: boolean;
31 |
32 | useIcon: boolean;
33 | }
34 |
35 | class PathfindingApp extends React.Component {
36 | // expose grid to parent to connect to button siblings
37 | private visualizer: RefObject = React.createRef();
38 |
39 | // drop down refs needed to invoke behavior between dropdowns
40 | private algDropDown: RefObject = React.createRef();
41 | private clrDropDown: RefObject = React.createRef();
42 | private mazeDropDown: RefObject = React.createRef();
43 | private tilesDropDown: RefObject = React.createRef();
44 |
45 | private readonly tileWidth: number;
46 |
47 | constructor(props: Props) {
48 | super(props);
49 | const settings = getDefaultSettings();
50 | this.state = {
51 | settings,
52 | heuristicDisabled: !PathfinderBuilder.usesHeuristic(settings.algorithm),
53 | bidirectionalDisabled: !settings.bidirectional,
54 | arrowsDisabled: false,
55 | scoreDisabled: false,
56 | panelShow: false,
57 | visualizing: false,
58 | paused: false,
59 | useIcon: this.useIcon(),
60 | };
61 | const mobile = isMobile();
62 | this.tileWidth = mobile ? 47 : 26;
63 | }
64 |
65 | windowOnResize = () => {
66 | this.setState({
67 | useIcon: this.useIcon(),
68 | });
69 | };
70 |
71 | componentDidMount() {
72 | window.addEventListener("resize", this.windowOnResize);
73 | }
74 |
75 | componentWillUnmount() {
76 | window.removeEventListener("resize", this.windowOnResize);
77 | }
78 |
79 | useIcon() {
80 | return window.innerWidth <= 850;
81 | }
82 |
83 | onClickAlgDrop() {
84 | this.clrDropDown.current!.hide();
85 | this.mazeDropDown.current!.hide();
86 | this.tilesDropDown.current!.hide();
87 | }
88 |
89 | onClickClrDrop() {
90 | this.algDropDown.current!.hide();
91 | this.mazeDropDown.current!.hide();
92 | this.tilesDropDown.current!.hide();
93 | }
94 |
95 | onClickMazeDrop() {
96 | this.clrDropDown.current!.hide();
97 | this.algDropDown.current!.hide();
98 | this.tilesDropDown.current!.hide();
99 | }
100 |
101 | onClickTilesDrop() {
102 | this.clrDropDown.current!.hide();
103 | this.algDropDown.current!.hide();
104 | this.mazeDropDown.current!.hide();
105 | }
106 |
107 | changeButtonActiveState(visualizing: boolean) {
108 | this.setState({
109 | visualizing: visualizing,
110 | });
111 | }
112 |
113 | toggleSettings() {
114 | this.setState((prevState) => ({
115 | panelShow: !prevState.panelShow,
116 | }));
117 | }
118 |
119 | hideSettings() {
120 | this.setState({
121 | panelShow: false,
122 | });
123 | }
124 |
125 | doPathfinding() {
126 | this.setState({
127 | paused: false,
128 | });
129 | this.visualizer.current!.doDelayedPathfinding();
130 | }
131 |
132 | pausePathfinding() {
133 | this.setState({
134 | paused: true,
135 | });
136 | this.visualizer.current!.pausePathfinding();
137 | }
138 |
139 | resumePathfinding() {
140 | this.setState({
141 | paused: false,
142 | });
143 | this.visualizer.current!.resumePathfinding();
144 | }
145 |
146 | clearPath() {
147 | this.visualizer.current!.clearPath();
148 | this.visualizer.current!.clearVisualizationChecked();
149 | }
150 |
151 | clearTiles() {
152 | this.clearPath();
153 | this.visualizer.current!.clearTilesChecked();
154 | }
155 |
156 | resetBoard() {
157 | this.clearPath();
158 | this.clearTiles();
159 | this.visualizer.current!.resetPoints();
160 | }
161 |
162 | createMaze() {
163 | this.visualizer.current!.createTerrain(MAZE, false);
164 | }
165 |
166 | createMazeVSkew() {
167 | this.visualizer.current!.createTerrain(MAZE_VERTICAL_SKEW, false);
168 | }
169 |
170 | createMazeHSkew() {
171 | this.visualizer.current!.createTerrain(MAZE_HORIZONTAL_SKEW, false);
172 | }
173 |
174 | createRandomTerrain() {
175 | this.visualizer.current!.createTerrain(RANDOM_TERRAIN, true);
176 | }
177 |
178 | changeTile(cost: number) {
179 | this.visualizer.current!.changeTile({
180 | isSolid: cost === -1,
181 | pathCost: cost,
182 | });
183 | }
184 |
185 | changeAlgo(algorithm: string) {
186 | this.setState((prevState) => ({
187 | heuristicDisabled: !PathfinderBuilder.usesHeuristic(algorithm),
188 | bidirectionalDisabled: !PathfinderBuilder.hasBidirectional(algorithm),
189 | scoreDisabled: !PathfinderBuilder.usesWeights(algorithm),
190 | settings: {
191 | ...prevState.settings,
192 | algorithm: algorithm,
193 | },
194 | }));
195 | }
196 |
197 | changeShowArrows() {
198 | this.setState((prevState) => ({
199 | settings: {
200 | ...prevState.settings,
201 | showArrows: !prevState.settings.showArrows,
202 | },
203 | }));
204 | }
205 |
206 | changeBidirectional() {
207 | this.setState((prevState) => ({
208 | settings: {
209 | ...prevState.settings,
210 | bidirectional: !prevState.settings.bidirectional,
211 | },
212 | }));
213 | }
214 |
215 | changeSpeed(value: number) {
216 | this.setState((prevState) => ({
217 | settings: {
218 | ...prevState.settings,
219 | delayInc: value,
220 | },
221 | }));
222 | }
223 |
224 | changeManhattan() {
225 | this.setState((prevState) => ({
226 | settings: {
227 | ...prevState.settings,
228 | heuristicKey: "manhattan",
229 | },
230 | }));
231 | }
232 |
233 | changeEuclidean() {
234 | this.setState((prevState) => ({
235 | settings: {
236 | ...prevState.settings,
237 | heuristicKey: "euclidean",
238 | },
239 | }));
240 | }
241 |
242 | changeChebyshev() {
243 | this.setState((prevState) => ({
244 | settings: {
245 | ...prevState.settings,
246 | heuristicKey: "chebyshev",
247 | },
248 | }));
249 | }
250 |
251 | changeOctile() {
252 | this.setState((prevState) => ({
253 | settings: {
254 | ...prevState.settings,
255 | heuristicKey: "octile",
256 | },
257 | }));
258 | }
259 |
260 | showTutorial() {
261 | localStorage.setItem(KEY_SHOW, "false");
262 | }
263 |
264 | render() {
265 | const title: string = "Pathfinding Visualizer";
266 | const icon = this.state.useIcon ? : title;
267 |
268 | return (
269 |
270 |
{getTutorialPages()}
271 |
this.hideSettings()} width={350} height={405}>
272 | this.changeShowArrows()}
277 | />
278 | this.changeSpeed(value)} initialSpeed={this.state.settings.delayInc} />
279 | this.changeBidirectional()}
283 | />
284 | this.changeManhattan()}
288 | onClickEuclidean={() => this.changeEuclidean()}
289 | onClickChebyshev={() => this.changeChebyshev()}
290 | onClickOctile={() => this.changeOctile()}
291 | />
292 |
293 |
294 | {
302 | this.showTutorial();
303 | window.location.reload();
304 | }}
305 | >
306 | {icon}
307 |
308 | this.onClickAlgDrop()}
311 | onChange={(alg: string) => this.changeAlgo(alg)}
312 | />
313 | this.onClickMazeDrop()}
316 | onClickMaze={() => this.createMaze()}
317 | onClickMazeHorizontal={() => this.createMazeHSkew()}
318 | onClickMazeVertical={() => this.createMazeVSkew()}
319 | onClickRandomTerrain={() => this.createRandomTerrain()}
320 | />
321 | this.pausePathfinding()}
325 | onResume={() => this.resumePathfinding()}
326 | onStartStop={() => this.doPathfinding()}
327 | />
328 | this.onClickClrDrop()}
331 | onClickTiles={() => this.clearTiles()}
332 | onClickPath={() => this.clearPath()}
333 | onClickReset={() => this.resetBoard()}
334 | />
335 | this.onClickTilesDrop()}
338 | onClickTileType={(cost: number) => this.changeTile(cost)}
339 | />
340 | this.toggleSettings()} />
341 |
342 |
this.changeButtonActiveState(viz)}
345 | settings={this.state.settings}
346 | tileWidth={this.tileWidth}
347 | />
348 |
349 | );
350 | }
351 | }
352 |
353 | function TopBar(props: PropsWithChildren<{}>) {
354 | return (
355 |
361 | {props.children}
362 |
363 | );
364 | }
365 |
366 | function isMobile() {
367 | return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
368 | }
369 |
370 | export default PathfindingApp;
371 |
--------------------------------------------------------------------------------
/src/components/PathfindingVisualizer.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React, { RefObject } from "react";
6 | import GridVisualization from "./GridVisualization";
7 | import GridForeground from "./GridForeground";
8 | import Stats from "./Stats";
9 | import GridBackground from "./GridBackground";
10 | import AppSettings from "../utils/AppSettings";
11 | import VirtualTimer from "../utils/VirtualTimer";
12 | import { createTileData, Point, Tile, TileData } from "../pathfinding/Core";
13 | import { PointSet } from "../pathfinding/Structures";
14 | import TerrainGeneratorBuilder, { PathfinderBuilder, RANDOM_TERRAIN } from "../pathfinding/Builders";
15 | import { Heuristics, Pathfinder, PathNode } from "../pathfinding/Pathfinders";
16 |
17 | interface Props {
18 | tileWidth: number;
19 | settings: Readonly;
20 | onChangeVisualizing: (visualizing: boolean) => void;
21 | }
22 |
23 | interface State {
24 | time: number;
25 | length: number;
26 | cost: number;
27 | nodes: number;
28 | algorithm: string;
29 | tilesX: number;
30 | tilesY: number;
31 | }
32 |
33 | class PathfindingVisualizer extends React.Component {
34 | // references to expose background and foreground grids to parent
35 | private background: RefObject = React.createRef();
36 | private foreground: RefObject = React.createRef();
37 |
38 | private visualized = false;
39 | private visualizing = false;
40 | private visualTimeouts: VirtualTimer[] = [];
41 | private generations: PathNode[] = [];
42 | private paused = false;
43 | private wasPaused = false; // paused before alt tab?
44 |
45 | private mazeTile: TileData = createTileData(true);
46 |
47 | private readonly tileWidth: number;
48 |
49 | constructor(props: Props) {
50 | super(props);
51 | const w = document.documentElement.clientWidth;
52 | const h = document.documentElement.clientHeight;
53 | this.tileWidth = this.props.tileWidth;
54 | const tilesX = Math.floor(w / this.tileWidth) + 1;
55 | const tilesY = Math.floor((h - 75 - 30) / this.tileWidth) + 1;
56 |
57 | this.state = {
58 | time: -1,
59 | length: -1,
60 | cost: -1,
61 | nodes: -1,
62 | algorithm: "",
63 | tilesX: tilesX,
64 | tilesY: tilesY,
65 | };
66 | }
67 |
68 | onWindowResize = () => {
69 | const w = document.documentElement.clientWidth;
70 | const h = document.documentElement.clientHeight;
71 | const tilesX = Math.floor(w / this.tileWidth) + 1;
72 | const tilesY = Math.floor((h - 75 - 30) / this.tileWidth) + 1;
73 |
74 | this.setState((prevState) => ({
75 | tilesX: prevState.tilesX < tilesX ? tilesX : prevState.tilesX,
76 | tilesY: prevState.tilesY < tilesY ? tilesY : prevState.tilesY,
77 | }));
78 | };
79 |
80 | onWindowBlur = () => {
81 | this.wasPaused = this.isPaused();
82 | if (!this.wasPaused) {
83 | this.pausePathfinding();
84 | }
85 | };
86 |
87 | onWindowFocus = () => {
88 | if (this.isPaused() && !this.wasPaused) {
89 | this.resumePathfinding();
90 | }
91 | };
92 |
93 | componentDidMount() {
94 | window.addEventListener("resize", this.onWindowResize);
95 | window.addEventListener("blur", this.onWindowBlur);
96 | window.addEventListener("focus", this.onWindowFocus);
97 | }
98 |
99 | componentWillUnmount() {
100 | window.removeEventListener("resize", this.onWindowResize);
101 | window.removeEventListener("blur", this.onWindowBlur);
102 | window.removeEventListener("focus", this.onWindowFocus);
103 | }
104 |
105 | shouldComponentUpdate(nextProps: Readonly, nextState: Readonly) {
106 | const prevState = this.state;
107 | const prevProps = this.props;
108 | return JSON.stringify(prevState) !== JSON.stringify(nextState) || JSON.stringify(prevProps) !== JSON.stringify(nextProps);
109 | }
110 |
111 | changeTile(data: TileData) {
112 | this.mazeTile = data; // enables weighted terrain
113 | this.foreground.current!.changeTile(data);
114 | }
115 |
116 | isPaused() {
117 | return this.paused;
118 | }
119 |
120 | pausePathfinding() {
121 | this.paused = true;
122 | for (const timeout of this.visualTimeouts) {
123 | timeout.pause();
124 | }
125 | }
126 |
127 | resumePathfinding() {
128 | this.paused = false;
129 | for (const timeout of this.visualTimeouts) {
130 | timeout.resume();
131 | }
132 | }
133 |
134 | doPathfinding() {
135 | this.clearPath();
136 | const settings = this.props.settings;
137 | const pathfinder = this.getPathfinder(settings);
138 | const path = this.findPath(pathfinder);
139 | this.generations = pathfinder.getRecentGenerations();
140 | this.visualizeGenerations(this.generations);
141 | this.addArrowGenerations(this.generations);
142 | this.drawPath(path);
143 | }
144 |
145 | doDelayedPathfinding() {
146 | const settings = this.props.settings;
147 | const background = this.background.current!;
148 |
149 | if (settings.delayInc < 20) {
150 | background.disableAnimations();
151 | } else {
152 | background.enableAnimations();
153 | }
154 |
155 | this.paused = false;
156 | this.clearVisualization();
157 | this.clearPath();
158 | this.visualized = false;
159 | const foreground = this.foreground.current!;
160 | foreground.toggleDisable();
161 |
162 | // start visualization if not visualizing
163 | if (!this.visualizing) {
164 | this.visualizing = true;
165 | this.props.onChangeVisualizing(this.visualizing);
166 |
167 | // perform actual shortest path calculation
168 | const pathfinder = this.getPathfinder(settings);
169 | const path = this.findPath(pathfinder);
170 |
171 | // initialize variables for visualization
172 | const promises: Promise[] = []; //to call function when timeouts finish
173 | this.visualTimeouts = [];
174 | const baseIncrement = settings.delayInc;
175 | let delay = 0;
176 | this.generations = pathfinder.getRecentGenerations();
177 | const grid = pathfinder.getNavigator().getGrid();
178 |
179 | // to keep track of rediscovered nodes
180 | const generationSet = new PointSet(grid.getWidth(), grid.getHeight());
181 |
182 | // each generation will be visualized on a timer
183 | this.generations.forEach((generation) => {
184 | const promise = new Promise((resolve) => {
185 | // each generation gets a higher timeout
186 | const timeout = new VirtualTimer(() => {
187 | this.visualizeGenerationAndArrows(generation);
188 | resolve(timeout);
189 | }, delay);
190 | this.visualTimeouts.push(timeout);
191 | });
192 | promises.push(promise);
193 | if (!generationSet.has(generation.tile.point)) {
194 | // rediscovered nodes shouldn't add a delay to visualization
195 | delay += baseIncrement;
196 | }
197 | generationSet.add(generation.tile.point);
198 | });
199 |
200 | // call functions when timeouts finish
201 | Promise.all(promises).then(() => {
202 | this.drawPath(path);
203 | foreground.toggleDisable();
204 | this.visualizing = false;
205 | this.visualized = true;
206 | this.props.onChangeVisualizing(this.visualizing);
207 | background.disableAnimations();
208 | });
209 | } else {
210 | // stop visualizing if currently visualizing
211 | for (const timeout of this.visualTimeouts) {
212 | timeout.clear();
213 | }
214 | this.visualizing = false;
215 | this.props.onChangeVisualizing(this.visualizing);
216 | }
217 | }
218 |
219 | getPathfinder(settings: AppSettings) {
220 | const algorithmKey = settings.algorithm;
221 | const algorithm =
222 | settings.bidirectional && PathfinderBuilder.hasBidirectional(algorithmKey)
223 | ? PathfinderBuilder.makeBidirectional(algorithmKey)
224 | : algorithmKey;
225 | return new PathfinderBuilder(this.foreground.current!.state.grid)
226 | .setAlgorithm(algorithm)
227 | .setHeuristic(settings.heuristicKey)
228 | .setNavigator(settings.navigatorKey)
229 | .build();
230 | }
231 |
232 | findPath(pathfinder: Pathfinder) {
233 | // perform pathfinding with performance calculation
234 | const foreground = this.foreground.current!;
235 | const t0 = performance.now();
236 | const path = pathfinder.findPath(foreground.state.initial, foreground.state.goal);
237 | const t1 = performance.now();
238 | const t2 = t1 - t0;
239 | this.setState({
240 | time: t2,
241 | nodes: pathfinder.getRecentNodes(),
242 | length: calcLength(foreground.state.initial, path),
243 | cost: calcCost(foreground.state.grid.get(foreground.state.initial), path),
244 | algorithm: pathfinder.getAlgorithmName(),
245 | });
246 | return path;
247 | }
248 |
249 | drawPath(path: Tile[]) {
250 | const foreground = this.foreground.current!;
251 | path.unshift(this.foreground.current!.state.grid.get(foreground.state.initial));
252 | this.foreground.current!.drawPath(path);
253 | }
254 |
255 | onTilesDragged() {
256 | if (this.visualized) {
257 | this.clearVisualization();
258 | this.doPathfinding();
259 | this.visualized = true;
260 | }
261 | }
262 |
263 | createTerrain(mazeType: number, useMazeTile: boolean) {
264 | if (this.visualizing) {
265 | return;
266 | }
267 | this.clearTiles();
268 | this.clearPath();
269 | this.clearVisualization();
270 | const foreground = this.foreground.current!;
271 | const end = this.calcEndPointInView();
272 |
273 | const newState =
274 | mazeType !== RANDOM_TERRAIN
275 | ? {
276 | initial: { x: 1, y: 1 },
277 | goal: { x: end.x - 2, y: end.y - 2 },
278 | }
279 | : {
280 | initial: { x: 1, y: ((end.y - 1) / 2) >> 0 },
281 | goal: { x: end.x - 2, y: ((end.y - 1) / 2) >> 0 },
282 | };
283 |
284 | foreground.setState(newState, () => {
285 | const prevGrid = foreground.state.grid;
286 |
287 | const solid: TileData = { pathCost: 1, isSolid: true };
288 |
289 | const generator = new TerrainGeneratorBuilder()
290 | .setDimensions(prevGrid.getWidth(), prevGrid.getHeight())
291 | .setGeneratorType(mazeType)
292 | .setIgnorePoints([foreground.state.initial, foreground.state.goal])
293 | .setTileData(useMazeTile ? this.mazeTile : solid)
294 | .build();
295 | const topLeft = { x: 1, y: 1 };
296 | const bottomRight = { x: end.x - 2, y: end.y - 2 };
297 | const grid = generator.generateTerrain(topLeft, bottomRight);
298 | foreground.drawGrid(grid);
299 | });
300 | }
301 |
302 | calcEndPointInView() {
303 | const end = this.calcEndPoint();
304 | const xEnd = end.x;
305 | const yEnd = end.y;
306 | const xFloor = Math.floor(xEnd);
307 | const yFloor = Math.floor(yEnd);
308 | const xDecimal = xEnd - xFloor;
309 | const yDecimal = yEnd - yFloor;
310 | let x = xDecimal > 0.05 ? Math.ceil(xEnd) : xFloor;
311 | let y = yDecimal > 0.05 ? Math.ceil(yEnd) : yFloor;
312 | if (x > this.state.tilesX) {
313 | x = this.state.tilesX;
314 | }
315 | if (y > this.state.tilesY) {
316 | y = this.state.tilesY;
317 | }
318 | return { x: x, y: y };
319 | }
320 |
321 | calcEndPoint() {
322 | const xEnd = Math.round(document.documentElement.clientWidth / this.tileWidth);
323 | const yEnd = Math.round((document.documentElement.clientHeight - 30 - 75) / this.tileWidth);
324 | return {
325 | x: xEnd,
326 | y: yEnd,
327 | };
328 | }
329 |
330 | resetPoints() {
331 | if (!this.visualizing) {
332 | this.foreground.current!.resetPoints();
333 | }
334 | }
335 |
336 | clearPath = () => {
337 | this.foreground.current!.erasePath();
338 | };
339 |
340 | clearTiles() {
341 | this.foreground.current!.clearTiles();
342 | }
343 |
344 | clearTilesChecked() {
345 | if (!this.visualizing) {
346 | this.foreground.current!.clearTiles();
347 | }
348 | }
349 |
350 | clearVisualization() {
351 | this.visualized = false;
352 | this.background.current!.clear();
353 | }
354 |
355 | clearVisualizationChecked() {
356 | if (!this.visualizing) {
357 | this.visualized = false;
358 | this.background.current!.clear();
359 | }
360 | }
361 |
362 | visualizeGenerations(generations: PathNode[]) {
363 | this.background.current!.visualizeGenerations(generations);
364 | this.visualized = true;
365 | }
366 |
367 | addArrowGenerations(generations: PathNode[]) {
368 | this.background.current!.addArrowGenerations(generations);
369 | }
370 |
371 | visualizeGenerationAndArrows(generation: PathNode) {
372 | this.background.current!.visualizeGenerationAndArrows(generation);
373 | }
374 |
375 | render() {
376 | return (
377 |
378 |
385 |
386 |
387 |
394 | this.onTilesDragged()}
397 | tileSize={this.tileWidth}
398 | width={this.state.tilesX}
399 | height={this.state.tilesY}
400 | end={this.calcEndPoint()}
401 | />
402 |
403 |
404 | );
405 | }
406 | }
407 |
408 | // calculates the length of a chain of tiles starting from a point
409 | function calcLength(initial: Point, path: Tile[]) {
410 | if (path.length === 0) {
411 | return 0;
412 | }
413 | let len = Heuristics.euclidean(initial, path[0].point);
414 | for (let i = 0; i < path.length - 1; i++) {
415 | len += Heuristics.euclidean(path[i].point, path[i + 1].point);
416 | }
417 | return +len.toFixed(3);
418 | }
419 |
420 | // calculates the cost of a chain of tiles starting from a point
421 | function calcCost(initial: Tile, path: Tile[]) {
422 | if (path.length === 0) {
423 | return 0;
424 | }
425 | let len = Heuristics.euclidean(initial.point, path[0].point) * path[0].data.pathCost;
426 | for (let i = 0; i < path.length - 1; i++) {
427 | len += Heuristics.euclidean(path[i].point, path[i + 1].point) * path[i + 1].data.pathCost;
428 | }
429 | return +len.toFixed(3);
430 | }
431 |
432 | export default PathfindingVisualizer;
433 |
--------------------------------------------------------------------------------
/src/components/RadioButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 |
7 | interface Props {
8 | boxStyle: string;
9 | defaultChecked: number;
10 | disabled: boolean;
11 | onChange: (() => void)[];
12 | }
13 |
14 | interface State {
15 | checked: boolean[];
16 | }
17 |
18 | class RadioButtonGroup extends React.Component {
19 | static defaultProps = { disabled: false };
20 |
21 | constructor(props: Props) {
22 | super(props);
23 | const checked: boolean[] = [];
24 | for (let i = 0; i < this.props.onChange.length; i++) {
25 | checked.push(i === this.props.defaultChecked);
26 | }
27 | this.state = { checked };
28 | }
29 |
30 | onChange(index: number) {
31 | const checked: boolean[] = [];
32 | for (let i = 0; i < this.props.onChange.length; i++) {
33 | checked.push(i === index);
34 | }
35 | this.setState({ checked }, () => this.props.onChange[index]());
36 | }
37 |
38 | render() {
39 | const children = React.Children.toArray(this.props.children);
40 | const radioButtons: JSX.Element[] = [];
41 | for (let i = 0; i < this.props.onChange.length; i++) {
42 | radioButtons.push(
43 |
44 | this.onChange(i)}
50 | />
51 | {children[i]}
52 |
53 | );
54 | }
55 | return radioButtons;
56 | }
57 | }
58 |
59 | export default RadioButtonGroup;
60 |
--------------------------------------------------------------------------------
/src/components/SettingPanels.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React from "react";
6 | import Checkbox from "./Checkbox";
7 | import RadioButtonGroup from "./RadioButtonGroup";
8 | import SteppedButtonRange from "./SteppedButtonRange";
9 |
10 | interface VisualProps {
11 | defaultShowArrows: boolean;
12 | onChangeShowArrows: () => void;
13 | disabledTree: boolean;
14 | disabledScore: boolean;
15 | }
16 |
17 | interface SpeedProps {
18 | onChange: (value: number) => void;
19 | initialSpeed: number;
20 | }
21 |
22 | interface AlgorithmProps {
23 | defaultAlg: boolean;
24 | onChangeBidirectional: (checked: boolean) => void;
25 | disabled: boolean;
26 | }
27 |
28 | interface HeuristicProps {
29 | defaultHeuristic: string;
30 | onClickManhattan: () => void;
31 | onClickEuclidean: () => void;
32 | onClickChebyshev: () => void;
33 | onClickOctile: () => void;
34 | disabled: boolean;
35 | }
36 |
37 | export const SPEED_STEP = 5;
38 | export const SPEED_MIN = 5;
39 | export const SPEED_MAX = 200;
40 |
41 | export function VisualSettings(props: VisualProps) {
42 | return (
43 |
44 |
Visualization
45 |
46 | Show Tree
47 |
48 |
49 | );
50 | }
51 |
52 | export class SpeedSettings extends React.Component {
53 | constructor(props: SpeedProps) {
54 | super(props);
55 | this.state = {
56 | speedText: String(this.props.initialSpeed),
57 | };
58 | }
59 |
60 | onChangeSpeed(value: number) {
61 | this.props.onChange(value);
62 | }
63 |
64 | render() {
65 | return (
66 |
67 |
Period
68 |
this.onChangeSpeed(value)}
75 | />
76 | ms
77 |
78 | );
79 | }
80 | }
81 |
82 | export function AlgorithmSettings(props: AlgorithmProps) {
83 | return (
84 |
85 |
Algorithm
86 |
87 | Bidirectional
88 |
89 |
90 | );
91 | }
92 |
93 | function getIndex(key: string) {
94 | switch (key) {
95 | case "manhattan":
96 | return 0;
97 | case "euclidean":
98 | return 1;
99 | case "chebyshev":
100 | return 2;
101 | case "octile":
102 | return 3;
103 | default:
104 | return 0;
105 | }
106 | }
107 |
108 | export function HeuristicSettings(props: HeuristicProps) {
109 | const index = getIndex(props.defaultHeuristic);
110 | return (
111 |
112 |
Heuristic
113 |
119 | Manhattan
120 | Euclidean
121 | Chebyshev
122 | Octile
123 |
124 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/src/components/Stats.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Joseph Prichard 2022.
3 | */
4 |
5 | import React, { RefObject } from "react";
6 |
7 | interface Props {
8 | algorithm: string;
9 | length: number;
10 | cost: number;
11 | time: number;
12 | nodes: number;
13 | }
14 |
15 | class Stats extends React.Component {
16 | private readonly textLog: RefObject = React.createRef();
17 |
18 | componentDidUpdate() {
19 | this.textLog.current!.scrollTop = this.textLog.current!.scrollHeight;
20 | }
21 |
22 | render() {
23 | const time = this.props.time.toFixed(2);
24 | const text =
25 | this.props.algorithm === ""
26 | ? ""
27 | : `${this.props.algorithm} visited ${this.props.nodes} nodes in ${time} ms. ` +
28 | `Path length = ${this.props.length}. Path cost = ${this.props.cost}.`;
29 | return (
30 |