├── .gitmodules ├── .bazelversion ├── .gitignore ├── screenshot.png ├── main.qrc ├── readme.md ├── RandomSolver.h ├── Solver.h ├── DijkstraSolver.h ├── WORKSPACE ├── RandomSolver.cpp ├── AStarSolver.h ├── main.cpp ├── StateSpace.cpp ├── StateSpace.h ├── BUILD ├── GraphWidget.h ├── DijkstraSolver.cpp ├── main.qml ├── AStarSolver.cpp └── GraphWidget.cpp /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 5.0.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justbuchanan/graph-algorithms-qt/HEAD/screenshot.png -------------------------------------------------------------------------------- /main.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | 5 | 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Graph Algorithms 2 | 3 | Dijkstra & A\* animation 4 | 5 | GUI allows for moving start/end points as well as adding obstacles. 6 | 7 | ![Screenshot](screenshot.png) 8 | 9 | ## Bazel config 10 | 11 | Bazel configuration is based on github.com/bbreslauer/qt-bazel-example 12 | -------------------------------------------------------------------------------- /RandomSolver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Solver.h" 4 | 5 | class RandomSolver : public Solver { 6 | public: 7 | RandomSolver(const StateSpace *ss, State start, State goal); 8 | 9 | void step() override; 10 | std::vector reconstructPath() override { 11 | // unimplemented 12 | return {}; 13 | } 14 | bool hasExplored(State s) const override; 15 | 16 | private: 17 | std::vector _explored; 18 | }; 19 | -------------------------------------------------------------------------------- /Solver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "StateSpace.h" 4 | 5 | class Solver { 6 | public: 7 | Solver(const StateSpace *ss, State start, State goal) 8 | : stateSpace(ss), start(start), goal(goal) {} 9 | 10 | virtual void step() = 0; 11 | virtual std::vector reconstructPath() = 0; 12 | virtual bool hasExplored(State s) const = 0; 13 | 14 | bool done() const { return hasExplored(goal); } 15 | 16 | const StateSpace *stateSpace; 17 | const State start, goal; 18 | }; 19 | -------------------------------------------------------------------------------- /DijkstraSolver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Solver.h" 4 | #include 5 | #include 6 | 7 | class DijkstraSolver : public Solver { 8 | public: 9 | DijkstraSolver(const StateSpace *ss, State start, State goal); 10 | 11 | void step() override; 12 | std::vector reconstructPath() override; 13 | bool hasExplored(State s) const override; 14 | 15 | protected: 16 | float tentativeDist(State s) const; 17 | 18 | private: 19 | std::set _unvisited; 20 | std::map _tentativeDist; 21 | std::map _prev; 22 | }; 23 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "com_justbuchanan_graph_algorithms_qt") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") 4 | 5 | git_repository( 6 | name = "com_justbuchanan_rules_qt", 7 | commit = "322bcced98e4ba54a1e908e1c86f8629b50cae40", 8 | remote = "https://github.com/justbuchanan/bazel_rules_qt", 9 | shallow_since = "1581319146 -0800", 10 | ) 11 | 12 | new_local_repository( 13 | name = "qt", 14 | build_file = "@com_justbuchanan_rules_qt//:qt.BUILD", 15 | path = "/usr/include/qt", # arch 16 | # path = "/usr/include/x86_64-linux-gnu/qt5", # debian 17 | ) 18 | -------------------------------------------------------------------------------- /RandomSolver.cpp: -------------------------------------------------------------------------------- 1 | #include "RandomSolver.h" 2 | 3 | #include 4 | #include 5 | 6 | RandomSolver::RandomSolver(const StateSpace *ss, State start, State goal) 7 | : Solver(ss, start, goal) { 8 | _explored.push_back(start); 9 | } 10 | 11 | void RandomSolver::step() { 12 | const State s = _explored[rand() % _explored.size()]; 13 | 14 | auto neighbors = stateSpace->neighborsOf(s); 15 | State neighbor = neighbors[rand() % neighbors.size()]; 16 | 17 | if (!hasExplored(neighbor)) { 18 | _explored.push_back(neighbor); 19 | } 20 | } 21 | 22 | bool RandomSolver::hasExplored(State s) const { 23 | return std::find(_explored.begin(), _explored.end(), s) != _explored.end(); 24 | } 25 | -------------------------------------------------------------------------------- /AStarSolver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Solver.h" 4 | #include 5 | #include 6 | 7 | class AStarSolver : public Solver { 8 | public: 9 | AStarSolver(const StateSpace *ss, State start, State goal); 10 | 11 | void step() override; 12 | std::vector reconstructPath() override; 13 | bool hasExplored(State s) const override; 14 | 15 | protected: 16 | float fScore(State s); 17 | float gScore(State s); 18 | 19 | float _heuristicCostEstimate(State a, State b); 20 | 21 | private: 22 | std::set _closedSet; 23 | std::set _openSet; 24 | std::map _cameFrom; 25 | // cost of getting from start to that node. Default value infinity 26 | std::map _gScore; 27 | 28 | // Default value infinity 29 | std::map _fScore; 30 | }; 31 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "GraphWidget.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char *argv[]) { 9 | QApplication app(argc, argv); 10 | app.setOrganizationName("justbuchanan"); 11 | app.setApplicationName("Graph Algorithms"); 12 | 13 | // load qml 14 | QQmlApplicationEngine engine(nullptr); 15 | engine.rootContext()->setContextProperty("main", &engine); 16 | 17 | qmlRegisterType("GraphWidget", 1, 0, "GraphWidget"); 18 | 19 | // load main.qml from qt resources - baked into binary as a QResource 20 | engine.load((QUrl("qrc:///main.qml"))); 21 | 22 | // get reference to main window from qml 23 | auto win = static_cast(engine.rootObjects()[0]); 24 | if (!win) { 25 | std::cerr << "Failed to load qml" << std::endl; 26 | exit(1); 27 | } 28 | 29 | win->show(); 30 | 31 | return app.exec(); 32 | } 33 | -------------------------------------------------------------------------------- /StateSpace.cpp: -------------------------------------------------------------------------------- 1 | #include "StateSpace.h" 2 | 3 | #include 4 | #include 5 | 6 | /// State 7 | 8 | std::ostream &operator<<(std::ostream &os, const State &s) { 9 | return os << "State{" << s.x << ", " << s.y << "}"; 10 | } 11 | 12 | bool operator<(const State &a, const State &b) { 13 | return a.x < b.x || (a.x == b.x && a.y < b.y); 14 | } 15 | 16 | bool operator==(const State &a, const State &b) { 17 | return a.x == b.x && a.y == b.y; 18 | } 19 | 20 | bool operator!=(const State &a, const State &b) { return !(a == b); } 21 | 22 | /// StateSpace 23 | 24 | StateSpace::StateSpace(int w, int h) : _w(w), _h(h) { 25 | // TODO: more c++ 26 | _backingArray = (bool *)malloc(sizeof(bool) * w * h); 27 | clearObstacles(); 28 | } 29 | 30 | void StateSpace::clearObstacles() { 31 | memset(_backingArray, 0, _w * _h * sizeof(bool)); 32 | } 33 | 34 | StateSpace::~StateSpace() { free(_backingArray); } 35 | 36 | std::vector StateSpace::neighborsOf(State s) const { 37 | std::vector ns; 38 | for (int i = -1; i < 2; i++) { 39 | for (int j = -1; j < 2; j++) { 40 | State n{s.x + i, s.y + j}; 41 | if (inBounds(n) && n != s && !obstacleAt(n)) { 42 | ns.push_back(n); 43 | } 44 | } 45 | } 46 | 47 | return ns; 48 | } 49 | -------------------------------------------------------------------------------- /StateSpace.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct State { 8 | int x, y; 9 | }; 10 | 11 | std::ostream &operator<<(std::ostream &os, const State &s); 12 | bool operator<(const State &a, const State &b); 13 | bool operator==(const State &a, const State &b); 14 | bool operator!=(const State &a, const State &b); 15 | 16 | // Maintains obstacles in a 2d discrete state space. 17 | class StateSpace { 18 | public: 19 | StateSpace(int w, int h); 20 | virtual ~StateSpace(); 21 | 22 | bool obstacleAt(State s) const { return _backingArray[s.x + _w * s.y]; } 23 | bool &obstacleAt(State s) { return _backingArray[s.x + _w * s.y]; } 24 | void setBlocked(State s, bool blocked = true) { obstacleAt(s) = blocked; } 25 | 26 | void clearObstacles(); 27 | 28 | int width() const { return _w; } 29 | int height() const { return _h; } 30 | 31 | bool inBounds(State s) const { 32 | return s.x >= 0 && s.x < _w && s.y >= 0 && s.y < _h; 33 | } 34 | 35 | // Returns all neighbors that are in bounds and not blocked by obstacles. 36 | std::vector neighborsOf(State s) const; 37 | 38 | float distance(State a, State b) const { 39 | return sqrtf(powf(a.x - b.x, 2) + powf(a.y - b.y, 2)); 40 | } 41 | 42 | private: 43 | int _w, _h; 44 | bool *_backingArray; 45 | }; 46 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | load("@com_justbuchanan_rules_qt//:qt.bzl", "qt_cc_library", "qt_resource") 2 | 3 | cc_binary( 4 | name = "main", 5 | srcs = ["main.cpp"], 6 | copts = [ 7 | "-fpic", 8 | ], 9 | deps = [ 10 | ":GraphWidget", 11 | ":StateSpace", 12 | ":main_qrc", 13 | "@qt//:qt_widgets", 14 | ], 15 | ) 16 | 17 | qt_resource( 18 | name = "main_qrc", 19 | qrc_file = "main.qrc", 20 | deps = [ 21 | "main.qml", 22 | ], 23 | ) 24 | 25 | qt_cc_library( 26 | name = "GraphWidget", 27 | src = "GraphWidget.cpp", 28 | hdr = "GraphWidget.h", 29 | deps = [ 30 | ":AStarSolver", 31 | ":DijkstraSolver", 32 | ":RandomSolver", 33 | ":StateSpace", 34 | "@qt//:qt_qml", 35 | "@qt//:qt_quick", 36 | ], 37 | ) 38 | 39 | cc_library( 40 | name = "StateSpace", 41 | srcs = ["StateSpace.cpp"], 42 | hdrs = ["StateSpace.h"], 43 | ) 44 | 45 | cc_library( 46 | name = "Solver", 47 | hdrs = ["Solver.h"], 48 | deps = [":StateSpace"], 49 | ) 50 | 51 | cc_library( 52 | name = "RandomSolver", 53 | srcs = ["RandomSolver.cpp"], 54 | hdrs = ["RandomSolver.h"], 55 | deps = [":Solver"], 56 | ) 57 | 58 | cc_library( 59 | name = "DijkstraSolver", 60 | srcs = ["DijkstraSolver.cpp"], 61 | hdrs = ["DijkstraSolver.h"], 62 | deps = [":Solver"], 63 | ) 64 | 65 | cc_library( 66 | name = "AStarSolver", 67 | srcs = ["AStarSolver.cpp"], 68 | hdrs = ["AStarSolver.h"], 69 | deps = [":Solver"], 70 | ) 71 | -------------------------------------------------------------------------------- /GraphWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Solver.h" 4 | #include "StateSpace.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class GraphWidget : public QQuickPaintedItem { 11 | Q_OBJECT 12 | 13 | public Q_SLOTS: 14 | void step(); 15 | 16 | void useDijkstra(); 17 | void useAstar(); 18 | void useRandom(); 19 | 20 | void clearObstacles(); 21 | 22 | public: 23 | GraphWidget(); 24 | 25 | Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY stepped) 26 | int iterations() const { return _iterations; } 27 | void setIterations(int i); 28 | void incItr(); 29 | 30 | Q_SIGNALS: 31 | void stepped(); 32 | 33 | protected: 34 | void paint(QPainter *painter) override; 35 | 36 | void geometryChanged(const QRectF &newGeometry, 37 | const QRectF &oldGeometry) override; 38 | 39 | void restartTimer(); 40 | void restartRun(); 41 | 42 | // mouse 43 | void mousePressEvent(QMouseEvent *event); 44 | void mouseMoveEvent(QMouseEvent *event); 45 | void mouseReleaseEvent(QMouseEvent *event); 46 | 47 | State _stateForPos(QPointF qp); 48 | 49 | void _useNamedSolver(std::string name); 50 | void _useNamedSolver(std::string name, State start, State goal); 51 | 52 | private: 53 | // if you click down on an obstacle, you enter erase mode. 54 | // if you click down where there's no obstacle, you enter draw mode. 55 | bool _erasingObstacles; 56 | 57 | enum { 58 | DraggingNone = 0, 59 | DraggingStart, 60 | DraggingGoal, 61 | DraggingObstacles, 62 | } _draggingItem; 63 | 64 | std::unique_ptr _stateSpace; 65 | std::unique_ptr _solver; 66 | QTimer _stepTimer; 67 | int _iterations = 0; 68 | 69 | std::vector _solutionPath; 70 | bool _solved = false; 71 | std::string _currentSolverName; 72 | }; 73 | -------------------------------------------------------------------------------- /DijkstraSolver.cpp: -------------------------------------------------------------------------------- 1 | #include "DijkstraSolver.h" 2 | 3 | #include 4 | #include 5 | 6 | DijkstraSolver::DijkstraSolver(const StateSpace *ss, State start, State goal) 7 | : Solver(ss, start, goal) { 8 | _tentativeDist[start] = 0; 9 | 10 | // mark all nodes unvisited 11 | for (int x = 0; x < stateSpace->width(); x++) { 12 | for (int y = 0; y < stateSpace->height(); y++) { 13 | State s{x, y}; 14 | 15 | if (!stateSpace->obstacleAt(s)) { 16 | _unvisited.insert(s); 17 | } 18 | } 19 | } 20 | } 21 | 22 | float DijkstraSolver::tentativeDist(State s) const { 23 | if (_tentativeDist.find(s) == _tentativeDist.end()) { 24 | return std::numeric_limits::max(); 25 | } 26 | 27 | return _tentativeDist.find(s)->second; 28 | } 29 | 30 | void DijkstraSolver::step() { 31 | // get unvisited node with smallest tentative dist 32 | const State current = *std::min_element( 33 | _unvisited.begin(), _unvisited.end(), [this](State s1, State s2) { 34 | return tentativeDist(s1) < tentativeDist(s2); 35 | }); 36 | 37 | for (State neighbor : stateSpace->neighborsOf(current)) { 38 | if (hasExplored(neighbor)) { 39 | continue; 40 | } 41 | 42 | // update tentative dist 43 | float new_dist = 44 | _tentativeDist[current] + stateSpace->distance(current, neighbor); 45 | if (new_dist < tentativeDist(neighbor)) { 46 | 47 | _tentativeDist[neighbor] = new_dist; 48 | _prev[neighbor] = current; 49 | } 50 | } 51 | 52 | _unvisited.erase(current); 53 | } 54 | 55 | std::vector DijkstraSolver::reconstructPath() { 56 | std::vector path; 57 | 58 | State current = goal; 59 | while (current != start) { 60 | path.push_back(current); 61 | current = _prev[current]; 62 | } 63 | 64 | path.push_back(start); 65 | // TODO: reverse 66 | 67 | return path; 68 | } 69 | 70 | bool DijkstraSolver::hasExplored(State s) const { 71 | return _unvisited.find(s) == _unvisited.end(); 72 | } 73 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Window 2.1 3 | import QtQuick.Controls 1.2 4 | import QtQuick.Layouts 1.2 5 | 6 | import GraphWidget 1.0 7 | 8 | ApplicationWindow { 9 | title: "Graph Algorithms" 10 | 11 | // makes window floating by default in tiling window managers 12 | modality: Qt.WindowModal 13 | 14 | ColumnLayout { 15 | anchors.fill: parent; 16 | 17 | // main toolbar 18 | Row{ 19 | padding: 5 20 | spacing: 5 21 | 22 | Layout.alignment: Qt.AlignTop 23 | 24 | Button { 25 | text: "Dijkstra / BFS" 26 | onClicked: graphWidget.useDijkstra() 27 | } 28 | 29 | Button { 30 | text: "A*" 31 | onClicked: graphWidget.useAstar() 32 | } 33 | 34 | // Button { 35 | // text: "Random" 36 | // onClicked: graphWidget.useRandom() 37 | // } 38 | 39 | Button { 40 | text: "Clear Obstacles" 41 | onClicked: graphWidget.clearObstacles() 42 | 43 | // red background color 44 | Rectangle { 45 | anchors.fill: parent 46 | anchors.margins: 1 47 | color: "red" 48 | opacity : .5 49 | } 50 | } 51 | } 52 | 53 | Row{ 54 | Layout.minimumHeight: 600 55 | Layout.minimumWidth: 800 56 | anchors.right: parent.right 57 | anchors.left: parent.left 58 | anchors.bottom: parent.bottom 59 | 60 | // draw graph 61 | GraphWidget { 62 | anchors.fill: parent 63 | 64 | id: graphWidget 65 | } 66 | } 67 | } 68 | 69 | // bottom bar 70 | statusBar: StatusBar { 71 | RowLayout { 72 | id: statusBarLayout 73 | anchors.fill: parent 74 | 75 | Label { 76 | text: "Iterations: " + graphWidget.iterations 77 | anchors.right: statusBarLayout.right 78 | } 79 | } 80 | } 81 | 82 | // keyboard shortcuts 83 | Shortcut { sequence: 'a'; onActivated: graphWidget.useAstar() } 84 | Shortcut { sequence: 'd'; onActivated: graphWidget.useDijkstra() } 85 | Shortcut { sequence: 'r'; onActivated: graphWidget.useRandom() } 86 | 87 | Shortcut { sequence: 'c'; onActivated: graphWidget.clearObstacles() } 88 | } 89 | -------------------------------------------------------------------------------- /AStarSolver.cpp: -------------------------------------------------------------------------------- 1 | #include "AStarSolver.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | AStarSolver::AStarSolver(const StateSpace *ss, State start, State goal) 8 | : Solver(ss, start, goal) { 9 | _openSet.insert(start); 10 | _gScore[start] = 0; 11 | _fScore[start] = _heuristicCostEstimate(start, goal); 12 | } 13 | 14 | float AStarSolver::fScore(State s) { 15 | if (_fScore.find(s) == _fScore.end()) { 16 | return std::numeric_limits::max(); 17 | } 18 | 19 | return _fScore[s]; 20 | } 21 | 22 | float AStarSolver::gScore(State s) { 23 | if (_gScore.find(s) == _gScore.end()) { 24 | return std::numeric_limits::max(); 25 | } 26 | 27 | return _gScore[s]; 28 | } 29 | 30 | void AStarSolver::step() { 31 | if (_openSet.size() == 0) { 32 | std::cout << "done or failed" << std::endl; 33 | return; 34 | } 35 | 36 | // find item in openSet with lowest f score 37 | State c = *std::min_element(_openSet.begin(), _openSet.end(), 38 | [this](State s1, State s2) { 39 | float f1 = fScore(s1); 40 | float f2 = fScore(s2); 41 | 42 | if (std::abs(f1 - f2) < 0.0001) { 43 | // tie breaker: choose the node closest to the 44 | // goal 45 | return _heuristicCostEstimate(s1, goal) < 46 | _heuristicCostEstimate(s2, goal); 47 | } else { 48 | return f1 < f2; 49 | } 50 | }); 51 | 52 | _openSet.erase(c); 53 | _closedSet.insert(c); 54 | 55 | if (c == goal) { 56 | return; 57 | } 58 | 59 | for (auto neighbor : stateSpace->neighborsOf(c)) { 60 | if (_closedSet.find(neighbor) != _closedSet.end()) { 61 | continue; // this neighbor already evaluated 62 | } 63 | 64 | // add to open set if not in already 65 | _openSet.insert(neighbor); 66 | 67 | float tentative_gScore = gScore(c) + stateSpace->distance(c, neighbor); 68 | 69 | if (tentative_gScore >= gScore(neighbor)) { 70 | // this is not a better path 71 | continue; 72 | } 73 | 74 | _cameFrom[neighbor] = c; 75 | _gScore[neighbor] = tentative_gScore; 76 | _fScore[neighbor] = 77 | tentative_gScore + _heuristicCostEstimate(neighbor, goal); 78 | } 79 | } 80 | 81 | std::vector AStarSolver::reconstructPath() { 82 | std::vector path; 83 | 84 | State current = goal; 85 | 86 | path.push_back(current); 87 | 88 | while (_cameFrom.find(current) != _cameFrom.end()) { 89 | current = _cameFrom[current]; 90 | path.push_back(current); 91 | } 92 | 93 | // path.reverse(); TODO 94 | 95 | return path; 96 | } 97 | 98 | float AStarSolver::_heuristicCostEstimate(State a, State b) { 99 | // return abs(a.x - b.x) + abs(a.y - b.y); 100 | // return stateSpace->distance(a, b); 101 | 102 | int dx = std::abs(a.x - b.x); 103 | int dy = std::abs(a.y - b.y); 104 | 105 | int diag = std::min(dx, dy); 106 | int straight = dx + dy - 2 * diag; 107 | 108 | return straight * 1 + diag * sqrtf(2.0); 109 | } 110 | 111 | bool AStarSolver::hasExplored(State s) const { 112 | return std::find(_closedSet.begin(), _closedSet.end(), s) != _closedSet.end(); 113 | } 114 | -------------------------------------------------------------------------------- /GraphWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "GraphWidget.h" 2 | 3 | #include "AStarSolver.h" 4 | #include "DijkstraSolver.h" 5 | #include "RandomSolver.h" 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace std; 11 | 12 | namespace { 13 | 14 | static const float kSquareSize = 20; 15 | static const float kDrawingInset = kSquareSize / 2; 16 | 17 | static const QColor kBackgroundColor = Qt::white; 18 | 19 | void blockLine(StateSpace *ss, State start, State dt, int t) { 20 | for (int _t = 0; _t < t; _t++) { 21 | State s{start.x + _t * dt.x, start.y + _t * dt.y}; 22 | if (ss->inBounds(s)) { 23 | ss->obstacleAt(s) = true; 24 | } 25 | } 26 | } 27 | 28 | // If @pt is in bounds, returns @pt. Otherwise returns the nearest point that is 29 | // within the bounds of the given state space. 30 | State nearestInBounds(const StateSpace &ss, State pt) { 31 | if (ss.inBounds(pt)) { 32 | return pt; 33 | } 34 | 35 | pt.x = std::min(pt.x, ss.width() - 1); 36 | pt.y = std::min(pt.y, ss.height() - 1); 37 | 38 | return pt; 39 | } 40 | 41 | void copyObstacles(const StateSpace &from, StateSpace *to) { 42 | for (int x = 0; x < std::min(from.width(), to->width()); x++) { 43 | for (int y = 0; y < std::min(from.height(), to->height()); y++) { 44 | State s{x, y}; 45 | to->obstacleAt(s) = from.obstacleAt(s); 46 | } 47 | } 48 | } 49 | 50 | class SolverFactory { 51 | public: 52 | virtual Solver *create(StateSpace *stateSpace, State start, State goal) = 0; 53 | }; 54 | 55 | template class SolverFactoryImpl : public SolverFactory { 56 | public: 57 | Solver *create(StateSpace *stateSpace, State start, State goal) override { 58 | return new SOLVER(stateSpace, start, goal); 59 | } 60 | }; 61 | 62 | std::unique_ptr createSolver(const string &name, StateSpace *stateSpace, 63 | State start, State goal) { 64 | static std::map solvers = { 65 | {"astar", new SolverFactoryImpl()}, 66 | {"dijkstra", new SolverFactoryImpl()}, 67 | {"random", new SolverFactoryImpl()}, 68 | }; 69 | 70 | return std::unique_ptr( 71 | solvers[name]->create(stateSpace, start, goal)); 72 | } 73 | 74 | } // namespace 75 | 76 | GraphWidget::GraphWidget() 77 | : _stateSpace(new StateSpace(100, 100)), _stepTimer(this) { 78 | // register for mouse events 79 | setAcceptedMouseButtons(Qt::LeftButton); 80 | _draggingItem = DraggingNone; 81 | 82 | _solved = false; 83 | 84 | // obstacles 85 | blockLine(_stateSpace.get(), State{12, 15}, State{1, 0}, 30); 86 | blockLine(_stateSpace.get(), State{60, 20}, State{0, 1}, 30); 87 | 88 | // timer to step the algorithm 89 | connect(&_stepTimer, SIGNAL(timeout()), this, SLOT(step())); 90 | 91 | // start out with A* and arbitrary start and end points 92 | _useNamedSolver("astar", {5, 5}, {30, 20}); 93 | } 94 | 95 | void GraphWidget::geometryChanged(const QRectF &newGeometry, 96 | const QRectF &oldGeometry) { 97 | 98 | // skip invalid geometry - this happens when the app is first launching. 99 | if (newGeometry.width() < 0.0001 || newGeometry.height() < 0.0001) { 100 | return; 101 | } 102 | 103 | // Use the largest grid we can fit in the new dimensions. 104 | const int w = floor((newGeometry.width() - kDrawingInset * 2) / kSquareSize); 105 | const int h = floor((newGeometry.height() - kDrawingInset * 2) / kSquareSize); 106 | 107 | // Create a state space with the new dimensions 108 | StateSpace *newStateSpace = new StateSpace(w, h); 109 | copyObstacles(*_stateSpace, newStateSpace); 110 | _stateSpace.reset(newStateSpace); 111 | 112 | // move start and goal to be in bounds 113 | State start = nearestInBounds(*newStateSpace, _solver->start); 114 | State goal = nearestInBounds(*newStateSpace, _solver->goal); 115 | 116 | // reset solver 117 | _useNamedSolver(_currentSolverName, start, goal); 118 | } 119 | 120 | void GraphWidget::restartRun() { 121 | _solutionPath.clear(); 122 | _solved = false; 123 | setIterations(0); 124 | _stepTimer.start(10); 125 | } 126 | 127 | void GraphWidget::_useNamedSolver(string name) { 128 | _useNamedSolver(name, _solver->start, _solver->goal); 129 | } 130 | 131 | void GraphWidget::_useNamedSolver(string name, State start, State goal) { 132 | _currentSolverName = name; 133 | _solver = createSolver(_currentSolverName, _stateSpace.get(), start, goal); 134 | restartRun(); 135 | } 136 | 137 | void GraphWidget::useDijkstra() { _useNamedSolver("dijkstra"); } 138 | 139 | void GraphWidget::useAstar() { _useNamedSolver("astar"); } 140 | 141 | void GraphWidget::useRandom() { _useNamedSolver("random"); } 142 | 143 | void GraphWidget::clearObstacles() { 144 | _stateSpace->clearObstacles(); 145 | restartRun(); 146 | } 147 | 148 | void GraphWidget::setIterations(int i) { 149 | if (i != _iterations) { 150 | _iterations = i; 151 | Q_EMIT stepped(); 152 | } 153 | } 154 | 155 | void GraphWidget::incItr() { 156 | setIterations(iterations() + 1); 157 | Q_EMIT stepped(); 158 | } 159 | 160 | // Triggered periodically by @_stepTimer 161 | void GraphWidget::step() { 162 | // Don't run while the user is adjusting the setup 163 | if (_draggingItem != DraggingNone) { 164 | return; 165 | } 166 | 167 | if (_solved) { 168 | return; 169 | } 170 | 171 | incItr(); 172 | _solver->step(); 173 | 174 | if (_solver->done()) { 175 | _solutionPath = _solver->reconstructPath(); 176 | _solved = true; 177 | _stepTimer.stop(); 178 | } 179 | 180 | update(); 181 | } 182 | 183 | void GraphWidget::paint(QPainter *painter) { 184 | painter->setRenderHint(QPainter::Antialiasing); 185 | 186 | // background white 187 | painter->setBrush(kBackgroundColor); 188 | painter->drawRect(QRectF{0, 0, width(), height()}); 189 | 190 | painter->translate(QPointF{kDrawingInset, kDrawingInset}); 191 | for (int x = 0; x < _stateSpace->width(); x++) { 192 | for (int y = 0; y < _stateSpace->height(); y++) { 193 | const State st = {x, y}; 194 | QColor c = QColor("#f1f1f1"); 195 | bool empty = true; 196 | if (st == _solver->goal) { 197 | c = QColor("#4CAF50"); // green 198 | empty = false; 199 | } else if (st == _solver->start) { 200 | c = QColor("#F44336"); // red 201 | empty = false; 202 | } else if (_stateSpace->obstacleAt(st)) { 203 | c = QColor(Qt::black); 204 | empty = false; 205 | } else if (_solved && 206 | std::find(_solutionPath.begin(), _solutionPath.end(), st) != 207 | _solutionPath.end()) { 208 | c = QColor("#2196F3"); // blue 209 | empty = false; 210 | } else if (_solver->hasExplored(st)) { 211 | c = QColor("#FFEB3B"); // yellow 212 | empty = false; 213 | } 214 | 215 | const QPointF center = 216 | QPointF((x + 0.5) * kSquareSize, (y + 0.5) * kSquareSize); 217 | 218 | if (empty) { 219 | painter->setBrush(Qt::black); 220 | 221 | QPen pen(Qt::black); 222 | pen.setWidth(1); 223 | painter->setPen(pen); 224 | 225 | painter->drawEllipse(center, 1, 1); 226 | continue; 227 | } else { 228 | painter->setBrush(c); 229 | painter->setPen(c); 230 | 231 | float rad = kSquareSize / 2.5; 232 | painter->drawEllipse(center, rad, rad); 233 | } 234 | } 235 | } 236 | } 237 | 238 | State GraphWidget::_stateForPos(QPointF qp) { 239 | return State{(int)(qp.x() / kSquareSize), (int)(qp.y() / kSquareSize)}; 240 | } 241 | 242 | void GraphWidget::mousePressEvent(QMouseEvent *event) { 243 | QPointF pos = event->pos() - QPointF(kDrawingInset, kDrawingInset); 244 | State state = _stateForPos(pos); 245 | 246 | if (state == _solver->start) { 247 | _draggingItem = DraggingStart; 248 | } else if (state == _solver->goal) { 249 | _draggingItem = DraggingGoal; 250 | } else { 251 | _draggingItem = DraggingObstacles; 252 | 253 | // If the user clicked on an obstacle, enter erase mode. Otherwise add 254 | // obstacles. 255 | _erasingObstacles = _stateSpace->obstacleAt(state); 256 | 257 | // toggle the obstacle state of clicked square 258 | _stateSpace->obstacleAt(state) = !_erasingObstacles; 259 | update(); 260 | } 261 | } 262 | 263 | void GraphWidget::mouseMoveEvent(QMouseEvent *event) { 264 | const State point = 265 | _stateForPos(event->pos() - QPointF(kDrawingInset, kDrawingInset)); 266 | 267 | if (_draggingItem == DraggingStart) { 268 | // reset the tree with the new start pos 269 | if (_solver->start != point) { 270 | _useNamedSolver(_currentSolverName, point, _solver->goal); 271 | } 272 | } else if (_draggingItem == DraggingGoal) { 273 | // set the new goal point 274 | if (_solver->goal != point) { 275 | _useNamedSolver(_currentSolverName, _solver->start, point); 276 | } 277 | } else if (_draggingItem == DraggingObstacles) { 278 | if (_stateSpace->inBounds(point)) { 279 | _stateSpace->setBlocked(point, !_erasingObstacles); 280 | } 281 | } 282 | 283 | if (_draggingItem != DraggingNone) { 284 | update(); 285 | } 286 | } 287 | 288 | void GraphWidget::mouseReleaseEvent(QMouseEvent *event) { 289 | _draggingItem = DraggingNone; 290 | } 291 | --------------------------------------------------------------------------------