diff --git a/src/main/java/de/oshgnacknak/gruphi/Canvas.java b/src/main/java/de/oshgnacknak/gruphi/Canvas.java new file mode 100644 index 0000000..f5289dc --- /dev/null +++ b/src/main/java/de/oshgnacknak/gruphi/Canvas.java @@ -0,0 +1,47 @@ +package de.oshgnacknak.gruphi; + +import javax.swing.*; +import java.awt.*; +import java.util.function.Consumer; + +public class Canvas extends JPanel { + + private final Consumer draw; + + public Canvas(Consumer draw) { + this.draw = draw; + } + + @Override + public void paint(Graphics graphics) { + var g = (Graphics2D) graphics; + + draw.accept(new Drawable() { + + @Override + public void fill(Color c) { + g.setColor(c); + } + + @Override + public void strokeWeight(double w) { + g.setStroke(new BasicStroke((int) w)); + } + + @Override + public void rect(double x, double y, double w, double h) { + g.fillRect((int) x, (int) y, (int) w, (int) h); + } + + @Override + public void ellipse(double x, double y, double w, double h) { + g.fillOval((int) (x - w/2), (int) (y - h/2), (int) w, (int) h); + } + + @Override + public void line(double x1, double y1, double x2, double y2) { + g.drawLine((int) x1, (int) y1, (int) x2, (int) y2); + } + }); + } +} diff --git a/src/main/java/de/oshgnacknak/gruphi/Drawable.java b/src/main/java/de/oshgnacknak/gruphi/Drawable.java new file mode 100644 index 0000000..25ee96b --- /dev/null +++ b/src/main/java/de/oshgnacknak/gruphi/Drawable.java @@ -0,0 +1,16 @@ +package de.oshgnacknak.gruphi; + +import java.awt.*; + +public interface Drawable { + + void fill(Color c); + + void strokeWeight(double w); + + void rect(double x, double y, double w, double h); + + void ellipse(double x, double y, double w, double h); + + void line(double x1, double y1, double x2, double y2); +} diff --git a/src/main/java/de/oshgnacknak/gruphi/Gruphi.java b/src/main/java/de/oshgnacknak/gruphi/Gruphi.java new file mode 100644 index 0000000..a284f66 --- /dev/null +++ b/src/main/java/de/oshgnacknak/gruphi/Gruphi.java @@ -0,0 +1,256 @@ +package de.oshgnacknak.gruphi; + +import h07.graph.DirectedGraph; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; +import java.util.Optional; + +class Gruphi extends JFrame { + + private static final int FRAME_DELAY = 1000 / 60; + private static final double VEL = 5; + private static final double NEIGHBOUR_DISTANCE = 50; + + DirectedGraph graph = newGraph(); + + MazeGenerator mazeGenerator = new MazeGenerator<>(graph, (a, b) -> + a.pos.distance(b.pos) <= NEIGHBOUR_DISTANCE); + + Node selected = null; + Point2D.Double vel = new Point2D.Double(0, 0); + private boolean running = true; + + Gruphi() { + super("Gruphi - The Graph GUI - By Osh"); + + var canvas = new Canvas(this::draw); + add(canvas); + pack(); + setLocationRelativeTo(null); + setDefaultCloseOperation(EXIT_ON_CLOSE); + + var mouseListener = new MouseAdapter() { + + @Override + public void mousePressed(MouseEvent e) { + switch (e.getButton()) { + case MouseEvent.BUTTON1: { + if (selected != null) { + var clicked = findClickedNode(e); + if (clicked.isPresent()) { + var n = clicked.get(); + if (graph.getChildrenForNode(selected).contains(n)) { + graph.disconnectNodes(selected, n); + } else { + graph.connectNodes(selected, 1.0, n); + } + } else { + selected.pos.x = e.getX(); + selected.pos.y = e.getY(); + } + } else { + graph.addNode(new Node(e.getX(), e.getY())); + } + } break; + case MouseEvent.BUTTON3: { + if (selected != null) { + selected.color = Node.COLOR; + selected.radius = Node.RADIUS; + selected = null; + } else { + findClickedNode(e) + .ifPresent(n -> { + selected = n; + selected.radius *= 2; + selected.color = Color.WHITE; + }); + if (selected != null) { + selected.color = Color.RED; + } + } + } break; + default: break; + } + } + }; + + addMouseListener(mouseListener); + addMouseMotionListener(mouseListener); + + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_K: + case KeyEvent.VK_W: + case KeyEvent.VK_UP: { + vel.y = -VEL; + } break; + + case KeyEvent.VK_J: + case KeyEvent.VK_S: + case KeyEvent.VK_DOWN: { + vel.y = VEL; + } break; + + case KeyEvent.VK_H: + case KeyEvent.VK_A: + case KeyEvent.VK_LEFT: { + vel.x = -VEL; + } break; + + case KeyEvent.VK_L: + case KeyEvent.VK_D: + case KeyEvent.VK_RIGHT: { + vel.x = VEL; + } break; + + case KeyEvent.VK_X: + case KeyEvent.VK_DELETE: + case KeyEvent.VK_BACK_SPACE: { + if (selected != null) { + graph.removeNode(selected); + selected = null; + } + } break; + + case KeyEvent.VK_Q: + case KeyEvent.VK_ESCAPE: { + running = false; + } break; + + case KeyEvent.VK_C: { + clearGraph(); + } break; + + case KeyEvent.VK_M: { + if (selected != null) { + mazeGenerator.generate(selected); + } + } break; + + case KeyEvent.VK_G: { + generateGrid(); + } break; + + default: break; + } + } + + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_K: + case KeyEvent.VK_J: + case KeyEvent.VK_W: + case KeyEvent.VK_S: + case KeyEvent.VK_DOWN: + case KeyEvent.VK_UP: { + vel.y = 0; + } break; + + case KeyEvent.VK_H: + case KeyEvent.VK_L: + case KeyEvent.VK_A: + case KeyEvent.VK_D: + case KeyEvent.VK_RIGHT: + case KeyEvent.VK_LEFT: { + vel.x = 0; + } break; + + default: break; + } + } + }); + } + + private void generateGrid() { + clearGraph(); + + var dist = NEIGHBOUR_DISTANCE; + + var rows = getHeight() / dist - 1; + var cols = getWidth() / dist - 1; + + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + var n = new Node((x + 0.5) * dist, (y + 0.5) * dist); + graph.addNode(n); + } + } + } + + private void clearGraph() { + selected = null; + for (var node : graph.getAllNodes()) { + graph.removeNode(node); + } + } + + private Optional findClickedNode(MouseEvent e) { + return graph.getAllNodes() + .stream() + .filter(n -> + n.inside(e.getX(), e.getY())) + .findFirst(); + } + + void draw(Drawable d) { + d.fill(Color.BLACK); + d.rect(0, 0, getWidth(), getHeight()); + + d.strokeWeight(2); + d.fill(Color.WHITE); + for (var node : graph.getAllNodes()) { + for (var child : graph.getChildrenForNode(node)) { + d.line(node.pos.x, node.pos.y, child.pos.x, child.pos.y); + } + } + + for (var node : graph.getAllNodes()) { + d.fill(node.color); + d.ellipse(node.pos.x, node.pos.y, node.radius * 2, node.radius * 2); + } + } + + private void updateLoop() { + var last = System.currentTimeMillis(); + var acc = 0; + + while (running) { + while (acc > FRAME_DELAY) { + update(); + acc -= FRAME_DELAY; + } + repaint(); + + var current = System.currentTimeMillis(); + acc += current - last; + last = current; + } + } + + void update() { + if (selected != null) { + selected.pos.x += vel.x; + selected.pos.y += vel.y; + } + } + + DirectedGraph newGraph() { + throw new UnsupportedOperationException("Return a h07.graph.DirectedGraphImpl here"); + } + + public static void main(String[] args) { + var gruphi = new Gruphi(); + gruphi.setVisible(true); + gruphi.updateLoop(); + System.exit(0); + } +} diff --git a/src/main/java/de/oshgnacknak/gruphi/MazeGenerator.java b/src/main/java/de/oshgnacknak/gruphi/MazeGenerator.java new file mode 100644 index 0000000..a6667d0 --- /dev/null +++ b/src/main/java/de/oshgnacknak/gruphi/MazeGenerator.java @@ -0,0 +1,61 @@ +package de.oshgnacknak.gruphi; + +import h07.graph.DirectedGraph; + +import java.util.*; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; + +public class MazeGenerator { + + private final Random random; + private final DirectedGraph graph; + private final Set visited; + private final BiPredicate areNeighbours; + + public MazeGenerator(DirectedGraph graph, BiPredicate areNeighbours) { + this.random = new Random(); + this.graph = graph; + this.areNeighbours = areNeighbours; + this.visited = new HashSet<>(); + } + + public void generate(V start) { + var stack = new Stack(); + visited.clear(); + + for (var node : graph.getAllNodes()) { + for (var child : graph.getChildrenForNode(node)) { + graph.disconnectNodes(node, child); + } + } + + visited.add(start); + stack.push(start); + while (!stack.isEmpty()) { + var current = stack.pop(); + var unvisited = getUnvisited(current); + + if (!unvisited.isEmpty()) { + stack.push(current); + var neighbour = unvisited.get(random.nextInt(unvisited.size())); + + graph.connectNodes(current, random.nextDouble(), neighbour); + graph.connectNodes(neighbour, random.nextDouble(), current); + + visited.add(neighbour); + stack.push(neighbour); + } + } + } + + private List getUnvisited(V node) { + return graph.getAllNodes() + .stream() + .filter(n -> + n != node + && areNeighbours.test(node, n) + && !visited.contains(n)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/oshgnacknak/gruphi/Node.java b/src/main/java/de/oshgnacknak/gruphi/Node.java new file mode 100644 index 0000000..9bec62d --- /dev/null +++ b/src/main/java/de/oshgnacknak/gruphi/Node.java @@ -0,0 +1,27 @@ +package de.oshgnacknak.gruphi; + +import java.awt.*; +import java.awt.geom.Point2D; + +class Node { + + public static final double RADIUS = 10; + + public static final Color COLOR = Color.WHITE; + + Point2D.Double pos; + + double radius; + + Color color; + + Node(double x, double y) { + this.pos = new Point2D.Double(x, y); + this.radius = RADIUS; + this.color = COLOR; + } + + boolean inside(double x, double y) { + return pos.distance(x, y) <= radius; + } +}