Graph Find Paths Iterative Algorithm

This commit is contained in:
dragonmacher 2019-04-11 16:48:29 -04:00
parent 00a5a4dc01
commit 7d722cd8c3
18 changed files with 1760 additions and 162 deletions

View File

@ -87,6 +87,10 @@ public class FunctionReachabilityTableModel
Accumulator<List<FRVertex>> pathAccumulator = new PassThroughAccumulator(accumulator);
if (v1.equals(v2)) {
return;
}
monitor.setMessage("Finding paths...");
GraphAlgorithms.findPaths(graph, v1, v2, pathAccumulator, monitor);
}

View File

@ -388,6 +388,7 @@ public class GraphAlgorithms {
* @param monitor the timeout task monitor
* @return the circuits
* @throws CancelledException if the monitor is cancelled
* @throws TimeoutException if the algorithm times-out, as defined by the monitor
*/
public static <V, E extends GEdge<V>> List<List<V>> findCircuits(GDirectedGraph<V, E> g,
boolean uniqueCircuits, TimeoutTaskMonitor monitor)
@ -405,10 +406,6 @@ public class GraphAlgorithms {
* <P><B><U>Warning:</U></B> for large, dense graphs (those with many interconnected
* vertices) this algorithm could run indeterminately, possibly causing the JVM to
* run out of memory.
*
* <P><B><U>Warning:</U></B> This is a recursive algorithm. As such, it is limited in how
* deep it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will
* not be found.
*
* <P>You are encouraged to call this method with a monitor that will limit the work to
* be done, such as the {@link TimeoutTaskMonitor}.
@ -423,8 +420,8 @@ public class GraphAlgorithms {
public static <V, E extends GEdge<V>> void findPaths(GDirectedGraph<V, E> g, V start, V end,
Accumulator<List<V>> accumulator, TaskMonitor monitor) throws CancelledException {
// the algorithm gets run at construction; the results are sent to the accumulator
new FindPathsAlgorithm<>(g, start, end, accumulator, monitor);
IterativeFindPathsAlgorithm<V, E> algo = new IterativeFindPathsAlgorithm<>();
algo.findPaths(g, start, end, accumulator, monitor);
}
/**
@ -436,10 +433,6 @@ public class GraphAlgorithms {
* <P><B><U>Warning:</U></B> for large, dense graphs (those with many interconnected
* vertices) this algorithm could run indeterminately, possibly causing the JVM to
* run out of memory.
*
* <P><B><U>Warning:</U></B> This is a recursive algorithm. As such, it is limited in how
* deep it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will
* not be found.
*
* @param g the graph
* @param start the start vertex
@ -453,8 +446,8 @@ public class GraphAlgorithms {
Accumulator<List<V>> accumulator, TimeoutTaskMonitor monitor)
throws CancelledException, TimeoutException {
// the algorithm gets run at construction; the results are sent to the accumulator
new FindPathsAlgorithm<>(g, start, end, accumulator, monitor);
FindPathsAlgorithm<V, E> algo = new IterativeFindPathsAlgorithm<>();
algo.findPaths(g, start, end, accumulator, monitor);
}
/**
@ -549,6 +542,7 @@ public class GraphAlgorithms {
* A method to debug the given graph by printing it.
*
* @param g the graph to print
* @param ps the output stream
*/
public static <V, E extends GEdge<V>> void printGraph(GDirectedGraph<V, E> g, PrintStream ps) {
Set<V> sources = getSources(g);

View File

@ -15,7 +15,7 @@
*/
package ghidra.graph.algo;
import java.util.*;
import java.util.List;
import ghidra.graph.GDirectedGraph;
import ghidra.graph.GEdge;
@ -23,122 +23,10 @@ import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Finds all paths between two vertices for a given graph.
*
* <P><B><U>Warning:</U></B> This is a recursive algorithm. As such, it is limited in how deep
* it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will not be found.
*
* <P>Note: this algorithm is based entirely on the {@link JohnsonCircuitsAlgorithm}.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class FindPathsAlgorithm<V, E extends GEdge<V>> {
public interface FindPathsAlgorithm<V, E extends GEdge<V>> {
public static final int JAVA_STACK_DEPTH_LIMIT = 2700;
private GDirectedGraph<V, E> g;
private V startVertex;
private V endVertex;
public void findPaths(GDirectedGraph<V, E> g, V start, V end, Accumulator<List<V>> accumulator,
TaskMonitor monitor) throws CancelledException;
private Stack<V> stack = new Stack<>();
private Set<V> blockedSet = new HashSet<>();
private Map<V, Set<V>> blockedBackEdgesMap = new HashMap<>();
public FindPathsAlgorithm(GDirectedGraph<V, E> g, V start, V end,
Accumulator<List<V>> accumulator, TaskMonitor monitor) throws CancelledException {
this.g = g;
this.startVertex = start;
this.endVertex = end;
monitor.initialize(g.getEdgeCount());
find(accumulator, monitor);
}
private void find(Accumulator<List<V>> accumulator, TaskMonitor monitor)
throws CancelledException {
explore(startVertex, accumulator, 0, monitor);
}
private boolean explore(V v, Accumulator<List<V>> accumulator, int depth, TaskMonitor monitor)
throws CancelledException {
// TODO
// Sigh. We are greatly limited in the size of paths we can processes due to the
// recursive nature of this algorithm. This should be changed to be non-recursive.
if (depth > JAVA_STACK_DEPTH_LIMIT) {
return false;
}
boolean foundPath = false;
blockedSet.add(v);
stack.push(v);
Collection<E> outEdges = getOutEdges(v);
for (E e : outEdges) {
monitor.checkCanceled();
V u = e.getEnd();
if (u.equals(endVertex)) {
outputCircuit(accumulator);
foundPath = true;
monitor.incrementProgress(1);
}
else if (!blockedSet.contains(u)) {
foundPath |= explore(u, accumulator, depth + 1, monitor);
monitor.incrementProgress(1);
}
}
if (foundPath) {
unblock(v);
}
else {
for (E e : outEdges) {
monitor.checkCanceled();
V u = e.getEnd();
addBackEdge(u, v);
}
}
stack.pop();
return foundPath;
}
private Collection<E> getOutEdges(V v) {
Collection<E> outEdges = g.getOutEdges(v);
if (outEdges == null) {
return Collections.emptyList();
}
return outEdges;
}
private void unblock(V v) {
blockedSet.remove(v);
Set<V> set = blockedBackEdgesMap.get(v);
if (set == null) {
return;
}
for (V u : set) {
if (blockedSet.contains(u)) {
unblock(u);
}
}
set.clear();
}
private void addBackEdge(V u, V v) {
Set<V> set = blockedBackEdgesMap.get(u);
if (set == null) {
set = new HashSet<>();
blockedBackEdgesMap.put(u, set);
}
set.add(v);
}
private void outputCircuit(Accumulator<List<V>> accumulator) {
List<V> path = new ArrayList<>(stack);
path.add(endVertex);
accumulator.add(path);
}
public void setStatusListener(GraphAlgorithmStatusListener<V> listener);
}

View File

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo;
/**
* An interface and state values used to follow the state of vertices as they are processed by
* algorithms
*
* @param <V> the vertex type
*/
public class GraphAlgorithmStatusListener<V> {
public enum STATUS {
WAITING, SCHEDULED, EXPLORING, BLOCKED, IN_PATH,
}
protected int totalStatusChanges;
public void statusChanged(V v, STATUS s) {
// stub
}
public void finished() {
// stub
}
public int getTotalStatusChanges() {
return totalStatusChanges;
}
}

View File

@ -0,0 +1,247 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo;
import java.util.*;
import org.apache.commons.collections4.map.LazyMap;
import org.apache.commons.collections4.set.ListOrderedSet;
import ghidra.graph.GDirectedGraph;
import ghidra.graph.GEdge;
import ghidra.graph.algo.GraphAlgorithmStatusListener.STATUS;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Finds all paths between two vertices for a given graph.
*
* <P>Note: this algorithm is based on the {@link JohnsonCircuitsAlgorithm}, modified to be
* iterative instead of recursive.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class IterativeFindPathsAlgorithm<V, E extends GEdge<V>>
implements FindPathsAlgorithm<V, E> {
private GDirectedGraph<V, E> g;
private V start;
private V end;
private Set<V> blockedSet = new HashSet<>();
private Map<V, Set<V>> blockedBackEdgesMap =
LazyMap.lazyMap(new HashMap<>(), k -> new HashSet<>());
private GraphAlgorithmStatusListener<V> listener = new GraphAlgorithmStatusListener<>();
private TaskMonitor monitor;
private Accumulator<List<V>> accumulator;
@Override
public void setStatusListener(GraphAlgorithmStatusListener<V> listener) {
this.listener = listener;
}
@SuppressWarnings("hiding") // squash warning on names of variables
@Override
public void findPaths(GDirectedGraph<V, E> g, V start, V end, Accumulator<List<V>> accumulator,
TaskMonitor monitor) throws CancelledException {
this.g = g;
this.start = start;
this.end = end;
this.accumulator = accumulator;
this.monitor = monitor;
if (start.equals(end)) {
// can't find the paths between a node and itself
throw new IllegalArgumentException("Start and end vertex cannot be the same: " + start);
}
if (!g.containsVertex(start)) {
throw new IllegalArgumentException("Start vertex is not in the graph: " + start);
}
if (!g.containsVertex(end)) {
throw new IllegalArgumentException("End vertex is not in the graph: " + end);
}
find();
listener.finished();
}
private void find() throws CancelledException {
Stack<Node> path = new Stack<>();
path.push(new Node(null, start));
monitor.initialize(g.getEdgeCount());
while (!path.isEmpty()) {
monitor.checkCanceled();
monitor.incrementProgress(1);
Node node = path.peek();
setStatus(node.v, STATUS.EXPLORING);
if (node.v.equals(end)) {
outputCircuit(path);
node.setParentFound();
path.pop();
}
else if (node.isExplored()) {
node.setDone();
path.pop();
}
else {
node = node.getNext();
path.push(node);
}
}
}
private void unblock(V v) {
ListOrderedSet<V> toProcess = new ListOrderedSet<>();
toProcess.add(v);
while (!toProcess.isEmpty()) {
V next = toProcess.remove(0);
Set<V> childBlocked = doUnblock(next);
if (childBlocked != null && !childBlocked.isEmpty()) {
toProcess.addAll(childBlocked);
childBlocked.clear();
}
}
}
private Set<V> doUnblock(V v) {
blockedSet.remove(v);
setStatus(v, STATUS.WAITING);
Set<V> set = blockedBackEdgesMap.get(v);
return set;
}
private void blockBackEdge(V u, V v) {
Set<V> set = blockedBackEdgesMap.get(u);
set.add(v);
}
private void outputCircuit(Stack<Node> stack) throws CancelledException {
List<V> path = new ArrayList<>();
for (Node vv : stack) {
path.add(vv.v);
}
setStatus(path, STATUS.IN_PATH);
accumulator.add(path);
monitor.checkCanceled(); // pause for listener
}
private void setStatus(List<V> path, STATUS s) {
for (V v : path) {
listener.statusChanged(v, s);
}
}
private void setStatus(V v, STATUS s) {
if (blockedSet.contains(v) && s == STATUS.WAITING) {
listener.statusChanged(v, STATUS.BLOCKED);
}
else {
listener.statusChanged(v, s);
}
}
private Collection<E> getOutEdges(V v) {
Collection<E> outEdges = g.getOutEdges(v);
if (outEdges == null) {
return Collections.emptyList();
}
return outEdges;
}
//==================================================================================================
// Inner Classes
//==================================================================================================
/**
* Simple class to maintain a relationship between a given node and its children that need
* processing. It also knows if it has been found in a path from start to end.
*/
private class Node {
private Node parent;
private V v;
private Deque<V> unexplored;
private boolean found;
Node(Node parent, V v) {
this.parent = parent;
this.v = v;
blockedSet.add(v);
setStatus(v, STATUS.SCHEDULED);
Collection<E> outEdges = getOutEdges(v);
unexplored = new ArrayDeque<>(outEdges.size());
for (E e : getOutEdges(v)) {
V u = e.getEnd();
if (!blockedSet.contains(u)) {
unexplored.add(u);
}
}
}
void setDone() {
if (found) {
setParentFound();
}
else {
// block back edges
for (E e : getOutEdges(v)) {
V u = e.getEnd();
blockBackEdge(u, v);
}
setStatus(v, STATUS.BLOCKED);
}
}
void setParentFound() {
if (parent != null) {
parent.found = true;
}
unblock(v);
}
boolean isExplored() {
return unexplored.isEmpty();
}
Node getNext() {
if (isExplored()) {
return null;
}
Node node = new Node(this, unexplored.pop());
return node;
}
@Override
public String toString() {
return v.toString();
}
}
}

View File

@ -0,0 +1,178 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo;
import java.util.*;
import ghidra.graph.GDirectedGraph;
import ghidra.graph.GEdge;
import ghidra.graph.algo.GraphAlgorithmStatusListener.STATUS;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Finds all paths between two vertices for a given graph.
*
* <P><B><U>Warning:</U></B> This is a recursive algorithm. As such, it is limited in how deep
* it can recurse. Any path that exceeds the {@link #JAVA_STACK_DEPTH_LIMIT} will not be found.
*
* <P>Note: this algorithm is based entirely on the {@link JohnsonCircuitsAlgorithm}.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class RecursiveFindPathsAlgorithm<V, E extends GEdge<V>>
implements FindPathsAlgorithm<V, E> {
public static final int JAVA_STACK_DEPTH_LIMIT = 2700;
private GDirectedGraph<V, E> g;
private V startVertex;
private V endVertex;
private Stack<V> stack = new Stack<>();
private Set<V> blockedSet = new HashSet<>();
private Map<V, Set<V>> blockedBackEdgesMap = new HashMap<>();
private Accumulator<List<V>> accumulator;
private TaskMonitor monitor;
private GraphAlgorithmStatusListener<V> listener = new GraphAlgorithmStatusListener<>();
@Override
public void setStatusListener(GraphAlgorithmStatusListener<V> listener) {
this.listener = listener;
}
@SuppressWarnings("hiding") // squash warning on names of variables
@Override
public void findPaths(GDirectedGraph<V, E> g, V start, V end, Accumulator<List<V>> accumulator,
TaskMonitor monitor) throws CancelledException {
this.g = g;
this.startVertex = start;
this.endVertex = end;
this.accumulator = accumulator;
this.monitor = monitor;
explore(startVertex, 0);
listener.finished();
}
private boolean explore(V v, int depth) throws CancelledException {
// TODO
// Sigh. We are greatly limited in the size of paths we can processes due to the
// recursive nature of this algorithm. This should be changed to be non-recursive.
if (depth > JAVA_STACK_DEPTH_LIMIT) {
return false;
}
boolean foundPath = false;
blockedSet.add(v);
stack.push(v);
setStatus(v, STATUS.EXPLORING);
Collection<E> outEdges = getOutEdges(v);
for (E e : outEdges) {
monitor.checkCanceled();
V u = e.getEnd();
if (u.equals(endVertex)) {
outputCircuit();
foundPath = true;
monitor.incrementProgress(1);
}
else if (!blockedSet.contains(u)) {
foundPath |= explore(u, depth + 1);
monitor.incrementProgress(1);
}
}
if (foundPath) {
unblock(v);
}
else {
for (E e : outEdges) {
monitor.checkCanceled();
V u = e.getEnd();
blockBackEdge(u, v);
}
}
stack.pop();
setStatus(v, STATUS.WAITING);
return foundPath;
}
private Collection<E> getOutEdges(V v) {
Collection<E> outEdges = g.getOutEdges(v);
if (outEdges == null) {
return Collections.emptyList();
}
return outEdges;
}
private void unblock(V v) {
blockedSet.remove(v);
setStatus(v, STATUS.WAITING);
Set<V> set = blockedBackEdgesMap.get(v);
if (set == null) {
return;
}
for (V u : set) {
if (blockedSet.contains(u)) {
unblock(u);
}
}
set.clear();
}
private void blockBackEdge(V u, V v) {
Set<V> set = blockedBackEdgesMap.get(u);
if (set == null) {
set = new HashSet<>();
blockedBackEdgesMap.put(u, set);
}
set.add(v);
setStatus(v, STATUS.BLOCKED);
}
private void outputCircuit() throws CancelledException {
List<V> path = new LinkedList<>(stack);
path.add(endVertex);
setStatus(path, STATUS.IN_PATH);
accumulator.add(path);
monitor.checkCanceled(); // pause for listener
setStatus(endVertex, STATUS.WAITING);
}
private void setStatus(List<V> path, STATUS s) {
for (V v : path) {
listener.statusChanged(v, s);
}
}
private void setStatus(V v, STATUS s) {
if (blockedSet.contains(v) && s == STATUS.WAITING) {
listener.statusChanged(v, STATUS.BLOCKED);
}
else {
listener.statusChanged(v, s);
}
}
}

View File

@ -722,10 +722,12 @@ public class VisualGraphPathHighlighter<V extends VisualVertex, E extends Visual
return t;
}
catch (InterruptedException e) {
Msg.trace(VisualGraphPathHighlighter.this, "Unable to complete the future", e);
Msg.trace(VisualGraphPathHighlighter.this,
"Unable to calculate graph path highlights - interrupted", e);
}
catch (ExecutionException e) {
Msg.debug(VisualGraphPathHighlighter.this, "Unable to complete the future", e);
Msg.debug(VisualGraphPathHighlighter.this, "Unable to calculate graph path highlights",
e);
}
return null;
}
@ -824,6 +826,10 @@ public class VisualGraphPathHighlighter<V extends VisualVertex, E extends Visual
private void calculatePathsBetweenVerticesAsync(V v1, V v2) {
if (v1.equals(v2)) {
return;
}
CallbackAccumulator<List<V>> accumulator = new CallbackAccumulator<>(path -> {
Collection<E> edges = pathToEdgesAsync(path);

View File

@ -33,6 +33,7 @@ import ghidra.graph.VisualGraph;
import ghidra.graph.viewer.*;
import ghidra.graph.viewer.layout.LayoutListener.ChangeType;
import ghidra.graph.viewer.renderer.ArticulatedEdgeRenderer;
import ghidra.graph.viewer.renderer.VisualGraphRenderer;
import ghidra.graph.viewer.shape.ArticulatedEdgeTransformer;
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
import ghidra.util.datastruct.WeakDataStructureFactory;
@ -308,8 +309,8 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
layoutLocations);
// DEGUG triggers grid lines to be printed; useful for debugging
// VisualGraphRenderer.DEBUG_ROW_COL_MAP.put((Graph<?, ?>) visualGraph,
// layoutLocations.copy());
// VisualGraphRenderer.DEBUG_ROW_COL_MAP.put((Graph<?, ?>) visualGraph,
// layoutLocations.copy());
Rectangle graphBounds =
getTotalGraphSize(vertexLayoutLocations, edgeLayoutArticulationLocations, transformer);

View File

@ -118,6 +118,7 @@ public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>
private void paintLayoutGridCells(RenderContext<V, E> renderContext, Layout<V, E> layout) {
// to enable this debug, search java files for commented-out uses of 'DEBUG_ROW_COL_MAP'
Graph<V, E> graph = layout.getGraph();
LayoutLocationMap<?, ?> locationMap = DEBUG_ROW_COL_MAP.get(graph);
if (locationMap == null) {

View File

@ -224,6 +224,34 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
return null;
}
protected void assertPathExists(List<List<TestV>> paths, TestV... vertices) {
List<TestV> expectedPath = List.of(vertices);
for (List<TestV> path : paths) {
if (path.equals(expectedPath)) {
return;
}
}
fail("List of paths does not contain: " + expectedPath + "\n\tactual paths: " + paths);
}
@SafeVarargs
protected final <V> void assertListEqualsOneOf(List<V> actual, List<V>... expected) {
StringBuilder buffy = new StringBuilder();
for (List<V> list : expected) {
if (areListsEquals(actual, list)) {
return;
}
buffy.append(list.toString());
}
fail("Expected : " + buffy + "\nActual: " + actual);
}
private <V> boolean areListsEquals(List<V> l1, List<V> l2) {
return l1.equals(l2);
}
//==================================================================================================
// Inner Classes
//==================================================================================================
@ -245,6 +273,34 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
return id;
}
// TODO put this in
//
// @Override
// public int hashCode() {
// final int prime = 31;
// int result = 1;
// result = prime * result + ((id == null) ? 0 : id.hashCode());
// return result;
// }
//
// @Override
// public boolean equals(Object obj) {
// if (this == obj) {
// return true;
// }
// if (obj == null) {
// return false;
// }
// if (getClass() != obj.getClass()) {
// return false;
// }
//
// TestV other = (TestV) obj;
// if (!Objects.equals(id, other.id)) {
// return false;
// }
// return true;
// }
}
protected static class TestE extends DefaultGEdge<TestV> {

View File

@ -26,7 +26,7 @@ import org.junit.Assert;
import org.junit.Test;
import ghidra.graph.algo.*;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.Msg;
import ghidra.util.datastruct.ListAccumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.TimeoutException;
@ -1105,23 +1105,6 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest {
Arrays.asList(v1, v2, v4, v5, v6, v3));
}
@SafeVarargs
private final <V> void assertListEqualsOneOf(List<V> actual, List<V>... expected) {
StringBuilder buffy = new StringBuilder();
for (List<V> list : expected) {
if (areListsEquals(actual, list)) {
return;
}
buffy.append(list.toString());
}
fail("Expected : " + buffy + "\nActual: " + actual);
}
private <V> boolean areListsEquals(List<V> l1, List<V> l2) {
return l1.equals(l2);
}
@Test
public void testDepthFirstPostOrder_MiddleAlternatingPaths() {
/*
@ -1362,19 +1345,355 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest {
}
@Test
public void testFindPaths_LimitRecursion() throws CancelledException {
public void testFindPaths_FullyConnected() throws CancelledException {
g = GraphFactory.createDirectedGraph();
TestV[] vertices =
generateSimplyConnectedGraph(FindPathsAlgorithm.JAVA_STACK_DEPTH_LIMIT + 100);
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v1 = vertices[0];
TestV v2 = vertices[vertices.length - 1];
edge(v1, v2);
edge(v1, v3);
Accumulator<List<TestV>> accumulator = new ListAccumulator<>();
edge(v2, v3);
edge(v2, v1);
edge(v3, v2);
edge(v3, v1);
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v2, accumulator, TaskMonitor.DUMMY);
assertTrue("The Find Paths algorithm should have failed, due to hitting the recursion " +
"limite, before finding a path", accumulator.isEmpty());
List<List<TestV>> paths = accumulator.asList();
assertPathExists(paths, v1, v2);
assertPathExists(paths, v1, v3, v2);
}
@Test
public void testFindPaths_FullyConnected2() throws CancelledException {
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
edge(v1, v2);
edge(v1, v3);
edge(v1, v4);
edge(v1, v5);
edge(v2, v1);
edge(v2, v3);
edge(v2, v4);
edge(v2, v5);
edge(v3, v1);
edge(v3, v2);
edge(v3, v4);
edge(v3, v5);
edge(v4, v1);
edge(v4, v2);
edge(v4, v3);
edge(v4, v5);
edge(v5, v1);
edge(v5, v2);
edge(v5, v3);
edge(v5, v4);
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v5, accumulator, TaskMonitor.DUMMY);
List<List<TestV>> paths = accumulator.asList();
assertEquals(16, paths.size());
assertPathExists(paths, v1, v5);
assertPathExists(paths, v1, v2, v5);
assertPathExists(paths, v1, v3, v5);
assertPathExists(paths, v1, v4, v5);
assertPathExists(paths, v1, v2, v3, v5);
assertPathExists(paths, v1, v2, v4, v5);
assertPathExists(paths, v1, v3, v2, v5);
assertPathExists(paths, v1, v3, v4, v5);
assertPathExists(paths, v1, v4, v2, v5);
assertPathExists(paths, v1, v4, v3, v5);
assertPathExists(paths, v1, v2, v3, v4, v5);
assertPathExists(paths, v1, v2, v4, v3, v5);
assertPathExists(paths, v1, v3, v2, v4, v5);
assertPathExists(paths, v1, v3, v4, v2, v5);
assertPathExists(paths, v1, v4, v2, v3, v5);
assertPathExists(paths, v1, v4, v3, v2, v5);
}
@Test
public void testFindPaths_MultiPaths() throws CancelledException {
/*
v1
/ \
v2 v3
| / | \
| v4 v5 v6
| * | |
| | v7
| | | \
| | v8 v9
| | * |
\ | /
\ | /
\|/
v10
Paths:
v1, v2, v10
v1, v3, v5, v10
v1, v3, v6, v7, v9, v10
*/
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
TestV v6 = vertex(6);
TestV v7 = vertex(7);
TestV v8 = vertex(8);
TestV v9 = vertex(9);
TestV v10 = vertex(10);
edge(v1, v2);
edge(v1, v3);
edge(v2, v10);
edge(v3, v4);
edge(v3, v5);
edge(v3, v6);
edge(v5, v10);
edge(v6, v7);
edge(v7, v8);
edge(v7, v9);
edge(v9, v10);
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY);
List<List<TestV>> paths = accumulator.asList();
assertEquals(3, paths.size());
assertPathExists(paths, v1, v2, v10);
assertPathExists(paths, v1, v3, v5, v10);
assertPathExists(paths, v1, v3, v6, v7, v9, v10);
accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY);
}
@Test
public void testFindPathsNew_MultiPaths_BackFlows() throws CancelledException {
/*
--> v1
| / \
-v2 v3
/ | \
v4 v5 v6 <--
* | | |
| v7 |
| | \ |
| v8 v9 -
| *
|
v10
Paths: v1, v3, v5, v10
*/
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
TestV v6 = vertex(6);
TestV v7 = vertex(7);
TestV v8 = vertex(8);
TestV v9 = vertex(9);
TestV v10 = vertex(10);
edge(v1, v2);
edge(v1, v3);
edge(v2, v1); // back edge
edge(v3, v4);
edge(v3, v5);
edge(v3, v6);
edge(v5, v10);
edge(v6, v7);
edge(v7, v8);
edge(v7, v9);
edge(v9, v6); // back edge
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY);
List<List<TestV>> paths = accumulator.asList();
assertEquals(1, paths.size());
assertPathExists(paths, v1, v3, v5, v10);
}
@Test
public void testFindPathsNew_MultiPaths_LongDeadEnd() throws CancelledException {
/*
v1
/ \
v2 v3
| / | \
| v4 v5 v6
| | | |
| v11 | v7
| | | | \
| v12 | v8 v9
| * | * |
\ | /
\ | /
\ |/
v10
Paths:
v1, v2, v10
v1, v3, v5, v10
v1, v3, v6, v7, v9, v10
*/
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
TestV v6 = vertex(6);
TestV v7 = vertex(7);
TestV v8 = vertex(8);
TestV v9 = vertex(9);
TestV v10 = vertex(10);
TestV v11 = vertex(11);
TestV v12 = vertex(12);
edge(v1, v2);
edge(v1, v3);
edge(v2, v10);
edge(v3, v4);
edge(v3, v5);
edge(v3, v6);
edge(v4, v11);
edge(v11, v12);
edge(v5, v10);
edge(v6, v7);
edge(v7, v8);
edge(v7, v9);
edge(v9, v10);
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY);
List<List<TestV>> paths = accumulator.asList();
assertEquals(3, paths.size());
assertPathExists(paths, v1, v2, v10);
assertPathExists(paths, v1, v3, v5, v10);
assertPathExists(paths, v1, v3, v6, v7, v9, v10);
}
@Test
public void testFindPathsNew_MultiPaths() throws CancelledException {
/*
v1
/ \
v2 v3
| / | \
| v4 v5 v6
| * | |
| | v7
| | | \
| | v8 v9
| | * |
\ | /
\ | /
\|/
v10
Paths:
v1, v2, v10
v1, v3, v5, v10
v1, v3, v6, v7, v9, v10
*/
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
TestV v6 = vertex(6);
TestV v7 = vertex(7);
TestV v8 = vertex(8);
TestV v9 = vertex(9);
TestV v10 = vertex(10);
edge(v1, v2);
edge(v1, v3);
edge(v2, v10);
edge(v3, v4);
edge(v3, v5);
edge(v3, v6);
edge(v5, v10);
edge(v6, v7);
edge(v7, v8);
edge(v7, v9);
edge(v9, v10);
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY);
List<List<TestV>> paths = accumulator.asList();
assertEquals(3, paths.size());
assertPathExists(paths, v1, v2, v10);
assertPathExists(paths, v1, v3, v5, v10);
assertPathExists(paths, v1, v3, v6, v7, v9, v10);
accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, v4, v10, accumulator, TaskMonitor.DUMMY);
paths = accumulator.asList();
assertTrue(paths.isEmpty());
}
@Test
@ -1383,7 +1702,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest {
startMemoryMonitorThread(false);
g = GraphFactory.createDirectedGraph();
TestV[] vertices = generateCompletelyConnectedGraph(30); // this takes a while
TestV[] vertices = generateCompletelyConnectedGraph(15);
int timeout = 250;
TimeoutTaskMonitor monitor = TimeoutTaskMonitor.timeoutIn(timeout, TimeUnit.MILLISECONDS);
@ -1391,7 +1710,10 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest {
TestV start = vertices[0];
TestV end = vertices[vertices.length - 1];
try {
GraphAlgorithms.findPaths(g, start, end, new ListAccumulator<>(), monitor);
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
GraphAlgorithms.findPaths(g, start, end, accumulator, monitor);
Msg.debug(this, "Found paths " + accumulator.size());
fail("Did not timeout in " + timeout + " ms");
}
catch (TimeoutException e) {

View File

@ -0,0 +1,151 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph;
import static org.junit.Assert.assertEquals;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
import javax.swing.JFrame;
import org.junit.Test;
import ghidra.graph.algo.*;
import ghidra.graph.algo.viewer.*;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.ListAccumulator;
import ghidra.util.exception.CancelledException;
/**
* A tool, written as a junit, that allows the user to run a test and then use the UI to step
* through the given graph algorithm.
*/
public class GraphAlgorithmsVisualDebugger extends AbstractGraphAlgorithmsTest {
@Override
protected GDirectedGraph<TestV, TestE> createGraph() {
return GraphFactory.createDirectedGraph();
}
@Test
public void testFindPathsNew_MultiPaths_BackFlows_WithUI_IterativeFindPathsAlgorithm()
throws CancelledException {
FindPathsAlgorithm<TestV, TestE> algo = new IterativeFindPathsAlgorithm<>();
doTestFindPathsNew_MultiPaths_BackFlows_WithUI(algo);
}
@Test
public void testFindPathsNew_MultiPaths_BackFlows_WithUI_RecursiveFindPathsAlgorithm()
throws CancelledException {
FindPathsAlgorithm<TestV, TestE> algo = new RecursiveFindPathsAlgorithm<>();
doTestFindPathsNew_MultiPaths_BackFlows_WithUI(algo);
}
private void doTestFindPathsNew_MultiPaths_BackFlows_WithUI(
FindPathsAlgorithm<TestV, TestE> algo) throws CancelledException {
/*
--> v1
| / \
-v2 v3
/ | \
v4 v5 v6 <--
* | | |
| v7 |
| | \ |
| v8 v9 -
| *
|
v10
Paths: v1, v3, v5, v10
*/
TestV v1 = vertex(1);
TestV v2 = vertex(2);
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
TestV v6 = vertex(6);
TestV v7 = vertex(7);
TestV v8 = vertex(8);
TestV v9 = vertex(9);
TestV v10 = vertex(10);
edge(v1, v2);
edge(v1, v3);
edge(v2, v1); // back edge
edge(v3, v4);
edge(v3, v5);
edge(v3, v6);
edge(v5, v10);
edge(v6, v7);
edge(v7, v8);
edge(v7, v9);
edge(v9, v6); // back edge
AlgorithmSteppingTaskMonitor steppingMonitor = new AlgorithmSteppingTaskMonitor();
steppingMonitor = new AlgorithmSelfSteppingTaskMonitor(500);
TestGraphAlgorithmSteppingViewerPanel<TestV, TestE> gp = showViewer(steppingMonitor);
algo.setStatusListener(gp.getStatusListener());
ListAccumulator<List<TestV>> accumulator = new ListAccumulator<>();
algo.findPaths(g, v1, v10, accumulator, steppingMonitor);
//Msg.debug(this, "Total status updates: " + gp.getStatusListener().getTotalStatusChanges());
steppingMonitor.pause(); // pause this thread to view the final output
List<List<TestV>> paths = accumulator.asList();
assertEquals(1, paths.size());
assertPathExists(paths, v1, v3, v5, v10);
}
private TestGraphAlgorithmSteppingViewerPanel<TestV, TestE> showViewer(
AlgorithmSteppingTaskMonitor steppingMonitor) {
String isHeadless = Boolean.toString(false);
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, isHeadless);
System.setProperty("java.awt.headless", isHeadless);
JFrame frame = new JFrame("Graph");
TestGraphAlgorithmSteppingViewerPanel<TestV, TestE> gp =
new TestGraphAlgorithmSteppingViewerPanel<>(g, steppingMonitor);
frame.getContentPane().add(gp);
frame.setSize(800, 800);
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
steppingMonitor.cancel();
}
});
return gp;
}
}

View File

@ -0,0 +1,51 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo.viewer;
import ghidra.util.Msg;
/**
* Stepping task monitor that will proceed to the next step after the specified delay
*/
public class AlgorithmSelfSteppingTaskMonitor extends AlgorithmSteppingTaskMonitor {
private int stepTime;
public AlgorithmSelfSteppingTaskMonitor(int stepTime) {
this.stepTime = stepTime;
}
@Override
public void pause() {
if (isCancelled()) {
return; // no pausing after cancelled
}
notifyStepReady();
synchronized (this) {
try {
wait(stepTime);
}
catch (InterruptedException e) {
Msg.debug(this, "Interrupted waiting for next step", e);
}
}
}
}

View File

@ -0,0 +1,89 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo.viewer;
import java.util.HashSet;
import java.util.Set;
import ghidra.generic.function.Callback;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitorAdapter;
/**
* Task monitor that will trigger a {@link #wait()} when {@link #checkCanceled()} is called. This
* allows clients to watch algorithms as they proceed.
*/
public class AlgorithmSteppingTaskMonitor extends TaskMonitorAdapter {
private Set<Callback> stepLisetners = new HashSet<>();
public AlgorithmSteppingTaskMonitor() {
setCancelEnabled(true);
}
public void addStepListener(Callback c) {
stepLisetners.add(c);
}
@Override
public void cancel() {
super.cancel();
step(); // wake-up any waiting threads
}
@Override
public void checkCanceled() throws CancelledException {
super.checkCanceled();
pause();
}
/**
* Causes this monitor to perform at {@link #wait()}. Call {@link #step()} to allow the
* client to continue.
*/
public void pause() {
if (isCancelled()) {
return; // no pausing after cancelled
}
notifyStepReady();
synchronized (this) {
try {
wait();
}
catch (InterruptedException e) {
Msg.debug(this, "Interrupted waiting for next step", e);
}
}
}
public void step() {
synchronized (this) {
notify();
}
}
protected void notifyStepReady() {
stepLisetners.forEach(l -> l.call());
}
}

View File

@ -0,0 +1,35 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo.viewer;
import ghidra.graph.viewer.edge.AbstractVisualEdge;
public class AlgorithmTestSteppingEdge<V>
extends AbstractVisualEdge<AlgorithmTestSteppingVertex<V>> {
AlgorithmTestSteppingEdge(AlgorithmTestSteppingVertex<V> start,
AlgorithmTestSteppingVertex<V> end) {
super(start, end);
}
// sigh. I could not get this to compile with 'V' type specified
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public AlgorithmTestSteppingEdge<V> cloneEdge(AlgorithmTestSteppingVertex start,
AlgorithmTestSteppingVertex end) {
return new AlgorithmTestSteppingEdge<>(start, end);
}
}

View File

@ -0,0 +1,183 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo.viewer;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Ellipse2D.Double;
import java.awt.image.BufferedImage;
import javax.swing.*;
import ghidra.graph.algo.GraphAlgorithmStatusListener.STATUS;
import ghidra.graph.graphs.AbstractTestVertex;
import ghidra.graph.viewer.vertex.VertexShapeProvider;
public class AlgorithmTestSteppingVertex<V> extends AbstractTestVertex
implements VertexShapeProvider {
private ShapeImage defaultShape;
private ShapeImage defaultWithPathShape;
private ShapeImage scheduledShape;
private ShapeImage exploringShape;
private ShapeImage blockedShape;
private ShapeImage currentShape;
private JLabel tempLabel = new JLabel();
private V v;
private STATUS status = STATUS.WAITING;
private boolean wasEverInPath;
protected AlgorithmTestSteppingVertex(V v) {
super(v.toString());
this.v = v;
buildShapes();
tempLabel.setText(v.toString());
}
public void setStatus(STATUS status) {
this.status = status;
ShapeImage si;
switch (status) {
case BLOCKED:
si = blockedShape;
if (wasEverInPath) {
si = defaultWithPathShape;
}
break;
case EXPLORING:
si = exploringShape;
break;
case SCHEDULED:
si = scheduledShape;
break;
case IN_PATH:
si = exploringShape;
wasEverInPath = true;
break;
case WAITING:
default:
si = defaultShape;
if (wasEverInPath) {
si = defaultWithPathShape;
}
break;
}
currentShape = si;
}
private void buildShapes() {
defaultShape = buildCircleShape(Color.LIGHT_GRAY, "default");
defaultWithPathShape = buildCircleShape(new Color(192, 216, 65), "default; was in path");
scheduledShape = buildCircleShape(new Color(255, 248, 169), "scheduled");
exploringShape = buildCircleShape(new Color(0, 147, 0), "exploring");
blockedShape = buildCircleShape(new Color(249, 190, 190), "blocked");
currentShape = defaultShape;
}
private ShapeImage buildCircleShape(Color color, String name) {
int w = 50;
int h = 50;
Double circle = new Ellipse2D.Double(0, 0, w, h);
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(color);
g2.fill(circle);
g2.dispose();
Dimension shapeSize = circle.getBounds().getSize();
int x = 50;
int y = 0;
circle.setFrame(x, y, shapeSize.width, shapeSize.height);
return new ShapeImage(image, circle, name);
}
V getTestVertex() {
return v;
}
@Override
public JComponent getComponent() {
ShapeImage si = getShapeImage();
ImageIcon icon = new ImageIcon(si.getImage());
tempLabel.setIcon(icon);
return tempLabel;
}
private ShapeImage getShapeImage() {
return currentShape;
}
@Override
public Shape getCompactShape() {
return getShapeImage().getShape();
}
@Override
public String toString() {
String statusString = status.toString();
if (wasEverInPath) {
statusString = "";
}
else if (status == STATUS.BLOCKED) {
statusString = "";
}
return v.toString() + " " + statusString;
}
private class ShapeImage {
private Image image;
private Shape shape;
private String shapeName;
ShapeImage(Image image, Shape shape, String name) {
this.image = image;
this.shape = shape;
this.shapeName = name;
}
Shape getShape() {
return shape;
}
Image getImage() {
return image;
}
String getName() {
return shapeName;
}
@Override
public String toString() {
return getName();
}
}
}

View File

@ -0,0 +1,347 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.graph.algo.viewer;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.*;
import java.util.List;
import javax.swing.*;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import edu.uci.ics.jung.visualization.decorators.EdgeShape;
import edu.uci.ics.jung.visualization.renderers.Renderer;
import ghidra.graph.*;
import ghidra.graph.algo.GraphAlgorithmStatusListener;
import ghidra.graph.graphs.DefaultVisualGraph;
import ghidra.graph.viewer.GraphViewer;
import ghidra.graph.viewer.GraphViewerUtils;
import ghidra.graph.viewer.layout.AbstractVisualGraphLayout;
import ghidra.graph.viewer.layout.GridLocationMap;
import ghidra.graph.viewer.options.VisualGraphOptions;
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
import ghidra.graph.viewer.vertex.VisualVertexRenderer;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.SwingUpdateManager;
import resources.ResourceManager;
public class TestGraphAlgorithmSteppingViewerPanel<V, E extends GEdge<V>> extends JPanel {
private GraphViewer<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> viewer;
private GDirectedGraph<V, E> graph;
private BidiMap<V, AlgorithmTestSteppingVertex<V>> vertexLookupMap = new DualHashBidiMap<>();
private List<BufferedImage> images = new ArrayList<>();
private JPanel phasesPanel;
private float zoom = .5f;
private SwingUpdateManager zoomRebuilder = new SwingUpdateManager(() -> rebuildImages());
private JPanel buttonPanel;
private AlgorithmSteppingTaskMonitor steppingMonitor;
private AbstractButton nextButton;
private GraphAlgorithmStatusListener<V> algorithmStatusListener =
new GraphAlgorithmStatusListener<>() {
public void finished() {
nextButton.setEnabled(true);
nextButton.setText("Done");
}
public void statusChanged(V v, STATUS s) {
totalStatusChanges++;
AlgorithmTestSteppingVertex<V> vv = vertexLookupMap.get(v);
vv.setStatus(s);
repaint();
addCurrentGraphToPhases();
}
private void addCurrentGraphToPhases() {
Rectangle layoutShape = GraphViewerUtils.getTotalGraphSizeInLayoutSpace(viewer);
Rectangle viewShape = GraphViewerUtils.translateRectangleFromLayoutSpaceToViewSpace(
viewer, layoutShape);
int w = viewShape.x + viewShape.width;
int h = viewShape.y + viewShape.height;
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D) image.getGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, w, h);
try {
SwingUtilities.invokeAndWait(() -> {
viewer.paint(g);
});
}
catch (Exception e) {
Msg.debug(this, "Unexpected exception", e);
}
images.add(image);
zoomRebuilder.updateLater();
}
};
public TestGraphAlgorithmSteppingViewerPanel(GDirectedGraph<V, E> graph,
AlgorithmSteppingTaskMonitor steppingMonitor) {
this.graph = graph;
this.steppingMonitor = steppingMonitor;
buildGraphViewer();
buildPhasesViewer();
buildButtons();
setLayout(new BorderLayout());
add(viewer, BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
steppingMonitor.addStepListener(() -> {
repaint();
nextButton.setEnabled(true);
});
}
public GraphAlgorithmStatusListener<V> getStatusListener() {
return algorithmStatusListener;
}
private void buildGraphViewer() {
TestGraph tvg = new TestGraph();
Collection<V> vertices = graph.getVertices();
for (V v : vertices) {
AlgorithmTestSteppingVertex<V> newV = new AlgorithmTestSteppingVertex<>(v);
tvg.addVertex(newV);
vertexLookupMap.put(v, newV);
}
Collection<E> edges = graph.getEdges();
for (E e : edges) {
V start = e.getStart();
V end = e.getEnd();
AlgorithmTestSteppingVertex<V> newStart = vertexLookupMap.get(start);
AlgorithmTestSteppingVertex<V> newEnd = vertexLookupMap.get(end);
AlgorithmTestSteppingEdge<V> newEdge =
new AlgorithmTestSteppingEdge<>(newStart, newEnd);
tvg.addEdge(newEdge);
}
TestGraphLayout layout = new TestGraphLayout(tvg);
tvg.setLayout(layout);
viewer = new GraphViewer<>(layout, new Dimension(400, 400));
viewer.setBackground(Color.WHITE);
viewer.setGraphOptions(new VisualGraphOptions());
Renderer<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> renderer =
viewer.getRenderer();
// TODO set renderer directly
renderer.setVertexRenderer(new VisualVertexRenderer<>());
// TODO note: this is needed to 1) use shapes and 2) center the vertices
VisualGraphVertexShapeTransformer<AlgorithmTestSteppingVertex<V>> shaper =
new VisualGraphVertexShapeTransformer<>();
viewer.getRenderContext().setVertexShapeTransformer(shaper);
viewer.getRenderContext().setEdgeShapeTransformer(EdgeShape.line(tvg));
viewer.getRenderContext().setVertexLabelTransformer(v -> v.toString());
}
private void buildPhasesViewer() {
JFrame f = new JFrame("Graph Phases");
JPanel parentPanel = new JPanel(new BorderLayout());
phasesPanel = new JPanel();
JPanel zoomPanel = new JPanel();
JButton inButton = new JButton("+");
inButton.addActionListener(e -> {
float newZoom = zoom + .1f;
zoom = Math.min(1f, newZoom);
zoomRebuilder.update();
});
JButton outButton = new JButton("-");
outButton.addActionListener(e -> {
float newZoom = zoom - .1f;
zoom = Math.max(0.1f, newZoom);
zoomRebuilder.update();
});
zoomPanel.add(inButton);
zoomPanel.add(outButton);
parentPanel.add(phasesPanel, BorderLayout.CENTER);
parentPanel.add(zoomPanel, BorderLayout.SOUTH);
f.getContentPane().add(parentPanel);
f.setSize(400, 400);
f.setVisible(true);
}
private void rebuildImages() {
phasesPanel.removeAll();
double scale = zoom;
images.forEach(image -> {
int w = image.getWidth();
int h = image.getHeight();
double sw = w * scale;
double sh = h * scale;
Image scaledImage = ResourceManager.createScaledImage(image, (int) sw, (int) sh,
Image.SCALE_AREA_AVERAGING);
JLabel label = new JLabel(new ImageIcon(scaledImage));
phasesPanel.add(label);
});
phasesPanel.invalidate();
phasesPanel.getParent().revalidate();
phasesPanel.repaint();
}
private void buildButtons() {
buttonPanel = new JPanel();
nextButton = new JButton("Next >>");
nextButton.addActionListener(e -> {
nextButton.setEnabled(false);
steppingMonitor.step();
});
nextButton.setEnabled(false);
buttonPanel.add(nextButton);
}
private class TestGraph extends
DefaultVisualGraph<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> {
private TestGraphLayout layout;
@Override
public TestGraphLayout getLayout() {
return layout;
}
public void setLayout(TestGraphLayout layout) {
this.layout = layout;
}
@Override
public TestGraph copy() {
TestGraph newGraph = new TestGraph();
Collection<AlgorithmTestSteppingVertex<V>> myVertices = getVertices();
for (AlgorithmTestSteppingVertex<V> v : myVertices) {
newGraph.addVertex(v);
}
Collection<AlgorithmTestSteppingEdge<V>> myEdges = getEdges();
for (AlgorithmTestSteppingEdge<V> e : myEdges) {
newGraph.addEdge(e);
}
return newGraph;
}
}
private class TestGraphLayout extends
AbstractVisualGraphLayout<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> {
protected TestGraphLayout(TestGraph graph) {
super(graph);
}
@SuppressWarnings("unchecked")
@Override
public VisualGraph<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> getVisualGraph() {
return (VisualGraph<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>>) getGraph();
}
@Override
protected boolean isCondensedLayout() {
return false;
}
@Override
protected GridLocationMap<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> performInitialGridLayout(
VisualGraph<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> g)
throws CancelledException {
GridLocationMap<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> grid =
new GridLocationMap<>();
// sort by name; assume name is just a number
List<AlgorithmTestSteppingVertex<V>> sorted = new ArrayList<>(g.getVertices());
Collections.sort(sorted, (v1, v2) -> {
Integer i1 = Integer.parseInt(v1.getName());
Integer i2 = Integer.parseInt(v2.getName());
return i1.compareTo(i2);
});
AlgorithmTestSteppingVertex<V> first = sorted.get(0);
assignRows(first, g, grid, 1, 1);
return grid;
}
private void assignRows(AlgorithmTestSteppingVertex<V> v,
VisualGraph<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> g,
GridLocationMap<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> grid,
int row, int col) {
int existing = grid.row(v);
if (existing > 0) {
return; // already processed
}
grid.row(v, row);
grid.col(v, col);
int nextRow = row++;
Collection<AlgorithmTestSteppingEdge<V>> children = g.getOutEdges(v);
int n = children.size();
int middle = n / 2;
int start = col - middle;
int childCol = start;
for (AlgorithmTestSteppingEdge<V> edge : children) {
AlgorithmTestSteppingVertex<V> child = edge.getEnd();
assignRows(child, g, grid, nextRow + 1, childCol++);
}
}
@Override
public AbstractVisualGraphLayout<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> createClonedLayout(
VisualGraph<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> newGraph) {
TestGraphLayout newLayout = new TestGraphLayout((TestGraph) newGraph);
return newLayout;
}
}
}

View File

@ -50,6 +50,8 @@ public class TestVisualGraph extends DefaultVisualGraph<AbstractTestVertex, Test
newGraph.addEdge(e);
}
newGraph.setLayout(layout);
return newGraph;
}
}