Compare commits

..

10 Commits

18 changed files with 511 additions and 14 deletions

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
src/main/java/h07
### VS-Code ###
.vscode/
*.code-workspace

View File

@ -45,6 +45,15 @@ Startknoten (grün) auswählen löscht Pfäde.
- Escape, `Q`:
Beenden
- `U`, `I`:
Gewichtung für die nächste Kante verkleinern/vergrößern
- `O`:
Gewichtung für die nächste Kante auf 1 zurücksetzten
- `F`:
Berechne oder verstecke den minimalen Spannbaum (blau)
## Wenn Knoten ausgewählt (rot)
- WASD:

View File

@ -75,6 +75,19 @@ public class Canvas extends JPanel {
r.run();
g.scale(1/scale, 1/scale);
}
@Override
public void text(double x, double y, String text) {
FontMetrics metrics = g.getFontMetrics(g.getFont());
int d = metrics.getAscent();
g.drawString(text, round(x), round(y) + d);
}
@Override
public void textSize(int size) {
var f = g.getFont();
g.setFont(new Font(f.getName(), Font.PLAIN, size));
}
});
}

View File

@ -23,4 +23,8 @@ public interface Drawable {
void translated(double x, double y, Runnable r);
void scaled(double scale, Runnable r);
void text(double x, double y, String text);
void textSize(int size);
}

View File

@ -1,5 +1,6 @@
package de.oshgnacknak.gruphi;
import h07.algorithm.MinimumSpanningForestAlgorithm;
import h07.algorithm.ShortestPathsAlgorithm;
import h07.graph.DirectedGraphFactory;
@ -38,4 +39,9 @@ public interface Gruphi {
* @return A {@link ShortestPathsAlgorithm} for the path finding
*/
ShortestPathsAlgorithm<Node, Double> getShortestPathsAlgorithm();
/**
* @return A {@link MinimumSpanningForestAlgorithm} for the spanning tree calculation
*/
MinimumSpanningForestAlgorithm<Node, Double> getMinimumSpanningForestAlgorithm();
}

View File

@ -1,5 +1,6 @@
package de.oshgnacknak.gruphi;
import h07.algorithm.MinimumSpanningForestAlgorithm;
import h07.algorithm.ShortestPathsAlgorithm;
import h07.graph.DirectedGraphFactory;
@ -12,6 +13,7 @@ public class GruphiBuilder {
private BiPredicate<Node, Node> neighbourPredicate;
private DirectedGraphFactory<Node, Double> directedGraphFactory;
private ShortestPathsAlgorithm<Node, Double> shortestPathsAlgorithm;
private MinimumSpanningForestAlgorithm<Node, Double> minimumSpanningForestAlgorithm;
public GruphiBuilder setFrameDelay(Long frameDelay) {
this.frameDelay = frameDelay;
@ -43,7 +45,12 @@ public class GruphiBuilder {
return this;
}
public GruphiBuilder setMinimumSpanningForestAlgorithm(MinimumSpanningForestAlgorithm<Node, Double> minimumSpanningForestAlgorithm) {
this.minimumSpanningForestAlgorithm = minimumSpanningForestAlgorithm;
return this;
}
public GruphiImpl createGruphi() {
return new GruphiImpl(frameDelay, velocity, gridSpacing, neighbourPredicate, directedGraphFactory, shortestPathsAlgorithm);
return new GruphiImpl(frameDelay, velocity, gridSpacing, neighbourPredicate, directedGraphFactory, shortestPathsAlgorithm, minimumSpanningForestAlgorithm);
}
}

View File

@ -31,6 +31,8 @@ class GruphiFrame extends JFrame {
private final DirectedGraph<Node, Double> graph;
private DirectedGraph<Node, Double> forest;
private final MazeGenerator<Node> mazeGenerator;
private final Gruphi gruphi;
@ -44,6 +46,7 @@ class GruphiFrame extends JFrame {
private Map<Node, Path<Node, Double>> paths = null;
private boolean running = true;
private boolean nuggets = false;
private double weight = 1.0;
GruphiFrame(Gruphi gruphi) {
super("Gruphi - The Graph GUI - By Osh");
@ -111,6 +114,7 @@ class GruphiFrame extends JFrame {
}
graph.removeNode(selected);
selected = null;
forest = null;
}
} break;
@ -141,6 +145,22 @@ class GruphiFrame extends JFrame {
generatePaths();
} break;
case KeyEvent.VK_I: {
weight = Math.min(weight*1.01, 10);
} break;
case KeyEvent.VK_U: {
weight = Math.max(weight/1.01, 0.1);
} break;
case KeyEvent.VK_O: {
weight = 1.0;
} break;
case KeyEvent.VK_F: {
generateForest();
} break;
default: break;
}
}
@ -186,6 +206,19 @@ class GruphiFrame extends JFrame {
});
}
private void generateForest() {
if (forest != null) {
forest = null;
return;
}
var algo = Objects.requireNonNull(
gruphi.getMinimumSpanningForestAlgorithm(),
"Did you supply a spanning forest implementation");
forest = algo.minimumSpanningForest(graph, Comparable::compareTo, gruphi.getDirectedGraphFactory());
}
private void generatePaths() {
clearPaths();
@ -212,7 +245,7 @@ class GruphiFrame extends JFrame {
if (graph.getChildrenForNode(selected).contains(n)) {
graph.disconnectNodes(selected, n);
} else {
graph.connectNodes(selected, 1.0, n);
graph.connectNodes(selected, weight, n);
}
} else {
selected.pos = v;
@ -265,6 +298,7 @@ class GruphiFrame extends JFrame {
private void clearGraph() {
selected = null;
forest = null;
clearPaths();
for (var node : graph.getAllNodes()) {
graph.removeNode(node);
@ -288,9 +322,12 @@ class GruphiFrame extends JFrame {
public void draw(Drawable d) {
d.fill(Color.BLACK);
d.rect(0, 0, getWidth(), getHeight());
panningAndZooming.draw(d, () ->
drawNodes(d));
d.text(10, 10, String.format("Weight: %.2f", weight));
}
private void drawNodes(Drawable d) {
@ -301,6 +338,21 @@ class GruphiFrame extends JFrame {
drawCells(d);
drawPath(d);
drawForest(d);
}
private void drawForest(Drawable d) {
if (forest == null) {
return;
}
d.strokeWeight(2);
d.fill(Color.BLUE);
for (var node : forest.getAllNodes()) {
for (var child : forest.getChildrenForNode(node)) {
d.line(node.pos.x, node.pos.y, child.pos.x, child.pos.y);
}
}
}
private void drawCells(Drawable d) {
@ -323,7 +375,6 @@ class GruphiFrame extends JFrame {
var path = paths.get(selected);
d.fill(Color.GREEN);
d.strokeWeight(2);
Node prev = null;
for (var node : path) {
if (prev != null) {
@ -336,6 +387,7 @@ class GruphiFrame extends JFrame {
private void drawConnections(Drawable d) {
for (var node : graph.getAllNodes()) {
for (var child : graph.getChildrenForNode(node)) {
d.strokeWeight(2 * graph.getArcWeightBetween(node, child));
d.line(node.pos.x, node.pos.y, child.pos.x, child.pos.y);
drawConnectionArrowHead(d, node, child);
}
@ -346,7 +398,7 @@ class GruphiFrame extends JFrame {
var v = child.pos
.copy()
.sub(node.pos);
var r = 4;
var r = 4 * graph.getArcWeightBetween(node, child);
var p = v.copy()
.setMag(v.mag() - child.radius)
.add(node.pos);

View File

@ -1,5 +1,6 @@
package de.oshgnacknak.gruphi;
import h07.algorithm.MinimumSpanningForestAlgorithm;
import h07.algorithm.ShortestPathsAlgorithm;
import h07.graph.DirectedGraphFactory;
@ -24,17 +25,20 @@ public class GruphiImpl implements Gruphi {
private final ShortestPathsAlgorithm<Node, Double> shortestPathsAlgorithm;
private final MinimumSpanningForestAlgorithm<Node, Double> minimumSpanningForestAlgorithm;
public GruphiImpl(Long frameDelay,
Double velocity,
Double gridSpacing,
BiPredicate<Node, Node> neighbourPredicate,
DirectedGraphFactory<Node, Double> directedGraphFactory, ShortestPathsAlgorithm<Node, Double> shortestPathsAlgorithm) {
DirectedGraphFactory<Node, Double> directedGraphFactory, ShortestPathsAlgorithm<Node, Double> shortestPathsAlgorithm, MinimumSpanningForestAlgorithm<Node, Double> minimumSpanningForestAlgorithm) {
this.frameDelay = frameDelay == null ? DEFAULT_FRAME_DELAY : frameDelay;
this.velocity = velocity == null ? DEFAULT_VELOCITY : velocity;
this.gridSpacing = gridSpacing == null ? DEFAULT_GRID_SPACING : gridSpacing;
this.neighbourPredicate = neighbourPredicate == null ? this::defaultAreNeighbours : neighbourPredicate;
this.directedGraphFactory = Objects.requireNonNull(directedGraphFactory, "A directedGraphFactory must be set");
this.shortestPathsAlgorithm = shortestPathsAlgorithm;
this.minimumSpanningForestAlgorithm = minimumSpanningForestAlgorithm;
}
private boolean defaultAreNeighbours(Node a, Node b) {
@ -70,4 +74,9 @@ public class GruphiImpl implements Gruphi {
public ShortestPathsAlgorithm<Node, Double> getShortestPathsAlgorithm() {
return shortestPathsAlgorithm;
}
@Override
public MinimumSpanningForestAlgorithm<Node, Double> getMinimumSpanningForestAlgorithm() {
return minimumSpanningForestAlgorithm;
}
}

View File

@ -1,11 +1,16 @@
package de.oshgnacknak.gruphi;
import h07.algorithm.Dijkstra;
import h07.algorithm.Kruskal;
import h07.graph.DirectedGraphFactory;
public class GruphiMain {
public static void main(String[] args) {
var gruphi = new GruphiBuilder()
// .setDirectedGraphFactory(someFactory)
// .setShortestPathsAlgorithm(somePathFinder)
.setDirectedGraphFactory(DirectedGraphFactory.defaultFactory())
.setShortestPathsAlgorithm(new Dijkstra<>())
.setMinimumSpanningForestAlgorithm(new Kruskal<>())
.createGruphi();
var frame = new GruphiFrame(gruphi);

View File

@ -40,8 +40,8 @@ public class MazeGenerator<V> {
stack.push(current);
var neighbour = unvisited.get(random.nextInt(unvisited.size()));
graph.connectNodes(current, random.nextDouble(), neighbour);
graph.connectNodes(neighbour, random.nextDouble(), current);
graph.connectNodes(current, 1.0, neighbour);
graph.connectNodes(neighbour, 1.0, current);
visited.add(neighbour);
stack.push(neighbour);

View File

@ -0,0 +1,96 @@
package h07.algorithm;
import h07.algebra.Monoid;
import h07.graph.DirectedGraph;
import h07.graph.Path;
import java.util.*;
import java.util.stream.Stream;
public class Dijkstra<V, A> implements ShortestPathsAlgorithm<V, A> {
@Override
public Map<V, Path<V, A>> shortestPaths(DirectedGraph<V, A> graph, V startNode, Monoid<A> monoid, Comparator<? super A> comparator) {
var d = new DijkstraImpl(graph, startNode, monoid, comparator);
d.computeShortestPaths();
return d.paths;
}
private class DijkstraImpl {
final DirectedGraph<V, A> graph;
final V startNode;
final Monoid<A> monoid;
final Comparator<? super A> comparator;
final Map<V, Path<V, A>> paths;
final Queue<V> queue;
private DijkstraImpl(DirectedGraph<V, A> graph, V startNode, Monoid<A> monoid, Comparator<? super A> comparator) {
this.graph = Objects.requireNonNull(graph, "Argument graph may not be null");
this.startNode = Objects.requireNonNull(startNode, "Argument startNode may not be null");
this.monoid = Objects.requireNonNull(monoid, "Argument monoid may not be null");
this.comparator = Objects.requireNonNull(comparator, "Argument comparator may not be null");
if (!graph.getAllNodes().contains(startNode)) {
throw new NoSuchElementException("Node startNode not in graph");
}
this.paths = getPathMap();
this.queue = getQueue();
}
private HashMap<V, Path<V, A>> getPathMap() {
var paths = new HashMap<V, Path<V, A>>();
paths.put(startNode, Path.of(startNode));
return paths;
}
private PriorityQueue<V> getQueue() {
var queue = new PriorityQueue<V>(Comparator.comparing(n ->
sumPathDistances(paths.get(n)), comparator));
queue.add(startNode);
return queue;
}
private A sumPathDistances(Path<?, A> path) {
return Stream.iterate(
path.traverser(),
Path.Traverser::hasNextNode,
t -> { t.walkToNextNode(); return t; })
.map(Path.Traverser::getDistanceToNextNode)
.reduce(monoid.zero(), monoid::add);
}
public void computeShortestPaths() {
while (!queue.isEmpty()) {
var node = queue.remove();
for (var child : graph.getChildrenForNode(node)) {
var path = getPath(node, child);
if (isShortestPath(child, path)) {
paths.put(child, path);
queue.remove(child);
queue.add(child);
}
}
}
}
private boolean isShortestPath(V child, Path<V, A> path) {
if (!paths.containsKey(child)) {
return true;
}
return 0 > comparator.compare(
sumPathDistances(path),
sumPathDistances(paths.get(child)));
}
private Path<V, A> getPath(V node, V child) {
return paths
.get(node)
.concat(child, graph.getArcWeightBetween(node, child));
}
}
}

View File

@ -0,0 +1,64 @@
package h07.algorithm;
import h07.graph.DirectedGraph;
import h07.graph.DirectedGraphFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
public class Kruskal<V, A> implements MinimumSpanningForestAlgorithm<V, A> {
@Override
public DirectedGraph<V, A> minimumSpanningForest(DirectedGraph<V, A> graph, Comparator<? super A> comparator, DirectedGraphFactory<V, A> factory) {
var output = factory.createDirectedGraph();
var sets = new ArrayList<Set<V>>();
for (V node : graph.getAllNodes()) {
output.addNode(node);
var set = new HashSet<V>();
set.add(node);
sets.add(set);
}
var egdes = graph.getAllNodes()
.stream()
.flatMap(n ->
graph.getChildrenForNode(n)
.stream()
.map(c ->
new Pair<>(n, c)))
.sorted(Comparator.comparing(
p -> graph.getArcWeightBetween(p.fst, p.snd),
comparator));
egdes.forEach(p -> {
var s1 = sets
.stream()
.filter(s -> s.contains(p.fst))
.findFirst()
.orElseThrow();
if (s1.contains(p.snd)) {
return;
}
var s2 = sets
.stream()
.filter(s -> s.contains(p.snd))
.findFirst()
.orElseThrow();
s1.addAll(s2);
sets.remove(s2);
var arc = graph.getArcWeightBetween(p.fst, p.snd);
output.connectNodes(p.fst, arc, p.snd);
output.connectNodes(p.snd, arc, p.fst);
});
return output;
}
}

View File

@ -0,0 +1,11 @@
package h07.algorithm;
import h07.graph.DirectedGraph;
import h07.graph.DirectedGraphFactory;
import java.util.Comparator;
public interface MinimumSpanningForestAlgorithm<V, A> {
DirectedGraph<V, A> minimumSpanningForest(DirectedGraph<V, A> graph, Comparator<? super A> comparator, DirectedGraphFactory<V, A> factory);
}

View File

@ -0,0 +1,11 @@
package h07.algorithm;
public class Pair<T, S> {
public T fst;
public S snd;
public Pair(T fst, S snd) {
this.fst = fst;
this.snd = snd;
}
}

View File

@ -13,4 +13,8 @@ public interface DirectedGraphFactory<V, A> {
* @return der {@code DirectedGraph}, der von dieses Fabrik erzeugt wird.
*/
DirectedGraph<V, A> createDirectedGraph();
static <V, A> DirectedGraphFactory<V, A> defaultFactory() {
return DirectedGraphImpl::new;
}
}

View File

@ -0,0 +1,91 @@
package h07.graph;
import java.util.*;
class DirectedGraphImpl<V, A> implements DirectedGraph<V, A> {
private final Map<V, Map<V, A>> theGraph;
DirectedGraphImpl() {
theGraph = new HashMap<>();
}
@Override
public Collection<V> getAllNodes() {
return Set.copyOf(theGraph.keySet());
}
@Override
public Collection<V> getChildrenForNode(V node) {
return Set.copyOf(getEdges(node).keySet());
}
private Map<V, A> getEdges(V node) {
var edges = theGraph.get(Objects.requireNonNull(node));
if (edges == null) {
throw new NoSuchElementException("Node not in graph: " + node);
}
return edges;
}
@Override
public A getArcWeightBetween(V from, V to) {
checkToNode(to);
var arc = getEdges(from).get(to);
if (arc == null) {
throw new NoSuchElementException("No edge from " + from + " to " + to);
}
return arc;
}
@Override
public void addNode(V node) {
Objects.requireNonNull(node);
if (theGraph.containsKey(node)) {
throw new IllegalArgumentException("Node already exists in graph: " + node);
}
theGraph.put(node, new HashMap<>());
}
@Override
public void removeNode(V node) {
Objects.requireNonNull(node);
if (theGraph.remove(node) == null) {
throw new NoSuchElementException("Node does not exist in graph: " + node);
}
for (var edges : theGraph.values()) {
edges.remove(node);
}
}
@Override
public void connectNodes(V from, A weight, V to) {
Objects.requireNonNull(from);
Objects.requireNonNull(weight);
checkToNode(to);
var edges = getEdges(from);
if (edges.containsKey(to)) {
throw new IllegalArgumentException("Edge from " + from + " to " + to + " already exists");
}
edges.put(to, weight);
}
@Override
public void disconnectNodes(V from, V to) {
Objects.requireNonNull(from);
checkToNode(to);
var edges = getEdges(from);
if (edges.remove(to) == null) {
throw new NoSuchElementException("No edge from " + from + " to " + to);
}
}
private void checkToNode(V to) {
Objects.requireNonNull(to);
if (!theGraph.containsKey(to)) {
throw new NoSuchElementException("Node not in graph: " + to);
}
}
}

View File

@ -95,11 +95,8 @@ public interface Path<V, A> extends Iterable<V> {
* @throws NullPointerException falls der Knoten {@code null} ist
*/
static <V, A> Path<V, A> of(V v1) {
throw new UnsupportedOperationException("Noch nicht implementiert.");
/*
Objects.requireNonNull(v1, "Der Knoten eines Pfades darf nicht null sein");
return new PathImpl<>(v1);
*/
}
/**

View File

@ -0,0 +1,120 @@
package h07.graph;
import java.util.*;
class PathImpl<V, A> implements Path<V, A> {
private final List<Node> thePath;
public PathImpl(V v1) {
Objects.requireNonNull(v1, "Argument v1 may not be null");
this.thePath = List.of(new Node(v1, null));
}
public PathImpl(List<Node> thePath) {
this.thePath = thePath;
}
@Override
public Traverser<V, A> traverser() {
return new TraverserImp();
}
@Override
public Path<V, A> concat(V node, A distance) {
Objects.requireNonNull(node, "Argument node may not be null");
Objects.requireNonNull(distance, "Argument distance may not be null");
var list = new ArrayList<>(thePath);
list.add(new Node(node, distance));
return new PathImpl<V, A>(list);
}
@Override
public Iterator<V> iterator() {
var iter = thePath.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public V next() {
return iter.next().node;
}
};
}
@Override
public String toString() {
var sb = new StringBuilder();
var trav = traverser();
while(trav.hasNextNode()) {
sb.append(trav.getCurrentNode())
.append(" -[")
.append(trav.getDistanceToNextNode())
.append("]-> ");
trav.walkToNextNode();
}
return sb
.append(trav.getCurrentNode())
.toString();
}
private class Node {
final V node;
final A distance;
private Node(V node, A distance) {
this.node = node;
this.distance = distance;
}
}
private class TraverserImp implements Traverser<V, A> {
Node current;
Node next;
Iterator<Node> iter;
TraverserImp() {
this.iter = thePath.iterator();
if (iter.hasNext()) {
current = iter.next();
}
if (iter.hasNext()) {
next = iter.next();
}
}
@Override
public V getCurrentNode() {
return current.node;
}
@Override
public A getDistanceToNextNode() {
if (next == null) {
throw new IllegalStateException("No next node present");
}
return next.distance;
}
@Override
public void walkToNextNode() {
if (next == null) {
throw new NoSuchElementException("No next node present");
}
current = next;
next = iter.hasNext() ? iter.next() : null;
}
@Override
public boolean hasNextNode() {
return next != null;
}
}
}