From 41c9bdb1f3177054ed675b0ab5a37e25d02067d4 Mon Sep 17 00:00:00 2001 From: Thibaud Date: Sat, 29 Sep 2018 19:05:19 +0200 Subject: [PATCH] Astar pathfinding in a grid --- pom.xml | 14 +++ simple-pathfinding/pom.xml | 15 +++ .../pathfinding/algorithm/AStarSearch.java | 91 +++++++++++++++++++ .../pathfinding/algorithm/AStarWorker.java | 35 +++++++ .../gasser/pathfinding/algorithm/Node.java | 46 ++++++++++ .../fr/uha/gasser/pathfinding/ui/App.java | 91 +++++++++++++++++++ .../fr/uha/gasser/pathfinding/ui/Grid.java | 81 +++++++++++++++++ 7 files changed, 373 insertions(+) create mode 100644 simple-pathfinding/pom.xml create mode 100644 simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarSearch.java create mode 100644 simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarWorker.java create mode 100644 simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/Node.java create mode 100644 simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/App.java create mode 100644 simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/Grid.java diff --git a/pom.xml b/pom.xml index e8eb020..e6c1fa2 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,20 @@ clock + simple-pathfinding + + + org.slf4j + slf4j-api + RELEASE + + + + org.slf4j + slf4j-simple + RELEASE + + \ No newline at end of file diff --git a/simple-pathfinding/pom.xml b/simple-pathfinding/pom.xml new file mode 100644 index 0000000..fb2913a --- /dev/null +++ b/simple-pathfinding/pom.xml @@ -0,0 +1,15 @@ + + + + swingapps + fr.uha.gasser + 1.0-SNAPSHOT + + 4.0.0 + + simple-pathfinding + + + \ No newline at end of file diff --git a/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarSearch.java b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarSearch.java new file mode 100644 index 0000000..880dc28 --- /dev/null +++ b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarSearch.java @@ -0,0 +1,91 @@ +package fr.uha.gasser.pathfinding.algorithm; + +import java.awt.*; +import java.util.*; +import java.util.List; + +class AStarSearch { + + private final Node start; + private final Node goal; + + AStarSearch(Node start, Node goal) { + this.start = start; + this.goal = goal; + } + + + private Node search() { + PriorityQueue frontier = new PriorityQueue<>(); + Set explored = new HashSet<>(); + frontier.add(start); + + while (!frontier.isEmpty()) { + Node currentNode = frontier.poll(); + explored.add(currentNode); + + if (currentNode.getLoc().equals(goal.getLoc())) { + return currentNode; + } + + for (Node neighbor : getNeighbors(currentNode)) { + // Ignore the neighbor which is already evaluated. + if (explored.contains(neighbor)) continue; + if (!frontier.contains(neighbor)) { + neighbor.setParent(currentNode); + neighbor.cost++; + neighbor.hValue = neighbor.cost + neighbor.loc.distance(goal.loc); + frontier.add(neighbor); + } + } + } + + return null; + } + + List shortestPath() { + Node currentNode = search(); + LinkedList path = new LinkedList<>(); + while (currentNode.getParent() != null) { + path.addFirst(currentNode); + currentNode = currentNode.getParent(); + } + + return path; + } + + private List getNeighbors(Node current) { + LinkedList neighbors = new LinkedList<>(); + for (Direction direction : Direction.values()) { + neighbors.add(getNeighbor(current, direction)); + } + + return neighbors; + } + + private Node getNeighbor(Node current, Direction direction) { + Node node = new Node(current); + node.setLoc(new Point(current.loc.x + direction.x, current.loc.y + direction.y)); + return node; + } + + private enum Direction { + UP(0, 1), + DOWN(0, -1), + LEFT(-1, 0), + LEFT_UP(-1, 1), + LEFT_DOWN(-1, -1), + RIGHT(1, 0), + RIGHT_UP(1, 1), + RIGHT_DOWN(1, -1); + + public final int x; + public final int y; + + Direction(int x, int y) { + this.x = x; + this.y = y; + } + } + +} \ No newline at end of file diff --git a/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarWorker.java b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarWorker.java new file mode 100644 index 0000000..0ff6b6b --- /dev/null +++ b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/AStarWorker.java @@ -0,0 +1,35 @@ +package fr.uha.gasser.pathfinding.algorithm; + +import fr.uha.gasser.pathfinding.ui.Grid; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +public class AStarWorker extends SwingWorker, List> { + private final Node start; + private final Node goal; + private final Grid target; + private List path; + + public AStarWorker(Node start, Node goal, Grid target) { + this.start = start; + this.goal = goal; + this.target = target; + } + + @Override + protected List doInBackground() throws Exception { + AStarSearch aStar = new AStarSearch(start, goal); + path = aStar.shortestPath(); + return path; + } + + @Override + protected void done() { + if (this.isCancelled()) return; + path.forEach(node -> { + if (! node.loc.equals(goal.loc)) target.setTile(node.loc, Color.BLUE); + }); + } +} diff --git a/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/Node.java b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/Node.java new file mode 100644 index 0000000..ac7cb85 --- /dev/null +++ b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/algorithm/Node.java @@ -0,0 +1,46 @@ +package fr.uha.gasser.pathfinding.algorithm; + +import java.awt.*; + +public class Node implements Comparable { + private Node parent; + Point loc; + int cost; + double hValue; // heuristic value + + public Node(Point loc) { + this.loc = loc; + } + + Node(Node other) { + this.parent = other.parent; + this.loc = other.loc; + this.cost = other.cost; + this.hValue = other.hValue; + } + + void setParent(Node parent) { + this.parent = parent; + } + + public void sethValue(double hValue) { + this.hValue = hValue; + } + + void setLoc(Point point) { + this.loc = point; + } + + public Point getLoc() { + return loc; + } + + Node getParent() { + return parent; + } + + @Override + public int compareTo(Node other) { + return Double.compare(this.hValue, other.hValue); + } +} diff --git a/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/App.java b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/App.java new file mode 100644 index 0000000..642cdb3 --- /dev/null +++ b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/App.java @@ -0,0 +1,91 @@ +package fr.uha.gasser.pathfinding.ui; + +import fr.uha.gasser.pathfinding.algorithm.AStarWorker; +import fr.uha.gasser.pathfinding.algorithm.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class App extends JFrame { + + private static final int ROWS = 30; + private static final int COLS = 30; + private static final int CELL_SIZE = 16; + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); + + private final Grid grid; + private AStarWorker pathfinder; + private Node startNode; + private Node endNode; + private JPanel gridPane; + + private App() throws HeadlessException { + super("Pathfinding"); + grid = new Grid(ROWS, COLS, CELL_SIZE); + setupInterface(); + pack(); + setVisible(true); + } + + private void setupInterface() { + gridPane = new JPanel(); + gridPane.setLayout(new GridLayout(ROWS, COLS)); + for (JLabel label : grid) { + gridPane.add(label); + } + + gridPane.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + final JLabel at = (JLabel) gridPane.getComponentAt(e.getPoint()); + final Point loc = grid.findLoc(at); + if (startNode == null) { + grid.setTile(loc, Color.RED); + startNode = new Node(loc); + } else if (endNode == null) { + grid.setTile(loc, Color.GREEN); + endNode = new Node(loc); + } + } + }); + + final JMenuBar menuBar = new JMenuBar(); + final JMenuItem runItem = new JMenuItem("Run"); + menuBar.add(runItem); + runItem.addActionListener(actionEvent -> doSearch()); + final JMenuItem cancelItem = new JMenuItem("Cancel"); + menuBar.add(cancelItem); + cancelItem.addActionListener(actionEvent -> doCancel()); + final JMenuItem resetItem = new JMenuItem("Reset"); + menuBar.add(resetItem); + resetItem.addActionListener(actionEvent -> doReset()); + + setJMenuBar(menuBar); + getContentPane().add(gridPane); + } + + private void doReset() { + pathfinder = null; + startNode = null; + endNode = null; + grid.clear(); + } + + private void doCancel() { + if (pathfinder == null) return; + pathfinder.cancel(true); + } + + private void doSearch() { + pathfinder = new AStarWorker(startNode, endNode, grid); + pathfinder.execute(); + } + + public static void main(String[] args) { + EventQueue.invokeLater(App::new); + } +} diff --git a/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/Grid.java b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/Grid.java new file mode 100644 index 0000000..f7b6ce9 --- /dev/null +++ b/simple-pathfinding/src/main/java/fr/uha/gasser/pathfinding/ui/Grid.java @@ -0,0 +1,81 @@ +package fr.uha.gasser.pathfinding.ui; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Iterator; + +public class Grid implements Iterable { + + private final static Border lineBorder = BorderFactory.createLineBorder(Color.BLACK, 1); + + private final JLabel[] cells; + private final Dimension size; + private int cellSize; + + Grid(int rows, int cols, int cellSize) { + this.cellSize = cellSize; + this.size = new Dimension(rows, cols); + int size = rows * cols; + this.cells = new JLabel[size]; + + initGrid(size); + } + + private void initGrid(int size) { + for (int i = 0; i < size; i++) { + final JLabel label = new JLabel(); + // Force cell size + label.setPreferredSize(new Dimension(cellSize, cellSize)); + label.setMinimumSize(label.getPreferredSize()); + label.setMaximumSize(label.getPreferredSize()); + label.setBorder(lineBorder); + label.setOpaque(true); + cells[i] = label; + } + } + + public Point findLoc(JLabel cell) { + for (int i = 0; i < cells.length; i++) { + JLabel label = cells[i]; + if (cell == label) return new Point(i % size.width, i / size.height); + } + // Not found + return new Point(-1, -1); + } + + public void setTile(Point loc, Color color) { + setTile(loc.x + size.width*loc.y, color); + } + + private void setTile(int index, Color color) { + final JLabel label = cells[index]; + label.setBackground(color); + label.revalidate(); + } + + void clear() { + for (JLabel cell : cells) { + cell.setBackground(null); + } + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private int pos = 0; + + @Override + public boolean hasNext() { + return cells.length > pos; + } + + @Override + public JLabel next() { + return cells[pos++]; + } + }; + } + +} \ No newline at end of file