├── .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 | 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 | 49 | 58 |
59 | ) : ( 60 | 63 | ); 64 | } 65 | 66 | export function SettingsButton({ onClick }: ButtonProps) { 67 | return ( 68 | 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} : 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 |