GP-4988 Created flow chart function graph layout

This commit is contained in:
ghidragon 2024-12-04 12:11:36 -05:00
parent 954ff4e124
commit 0a5a2ce9d8
60 changed files with 6570 additions and 1312 deletions

View file

@ -44,6 +44,8 @@ src/main/resources/images/fgpaths.png||GHIDRA||reviewed||END|
src/main/resources/images/fgrevblock.png||GHIDRA||reviewed||END|
src/main/resources/images/field.header.png||GHIDRA||reviewed|Custom icon|END|
src/main/resources/images/fullscreen_view.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/function_graph_flowchart.png||GHIDRA||||END|
src/main/resources/images/function_graph_flowchart_left.png||GHIDRA||||END|
src/main/resources/images/graph_view.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/id.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/paintbrush.png||FAMFAMFAM Icons - CC 2.5||||END|

View file

@ -21,6 +21,8 @@ color.bg.plugin.functiongraph.paint.icon = color.palette.lightcornflowerblue
icon.plugin.functiongraph.layout.experimental = package_development.png
icon.plugin.functiongraph.layout.flowchart = function_graph_flowchart.png
icon.plugin.functiongraph.layout.flowchart.left = function_graph_flowchart_left.png
icon.plugin.functiongraph.action.vertex.xrefs = brick_link.png
icon.plugin.functiongraph.action.vertex.maximize = fullscreen_view.png
icon.plugin.functiongraph.action.vertex.minimize = graph_view.png

View file

@ -442,7 +442,8 @@ public class FunctionGraph extends GroupingVisualGraph<FGVertex, FGEdge> {
public void setRootVertex(FGVertex rootVertex) {
if (this.rootVertex != null) {
throw new IllegalStateException("Cannot set the root vertex more than once!");
this.rootVertex = rootVertex;
return;
}
this.rootVertex = rootVertex;
@ -586,6 +587,7 @@ public class FunctionGraph extends GroupingVisualGraph<FGVertex, FGEdge> {
FGLayout originalLayout = getLayout();
FGLayout newLayout = originalLayout.cloneLayout(newGraph);
newGraph.rootVertex = rootVertex;
// setSize() must be called after setGraphLayout() due to callbacks performed when
// setSize() is called

View file

@ -34,7 +34,7 @@ import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.FlowType;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -184,7 +184,7 @@ public class FunctionGraphFactory {
private static String layoutGraph(Function function, FGController controller,
FunctionGraph functionGraph, TaskMonitor monitor) throws CancelledException {
if (!performSwingThreadRequiredWork(functionGraph)) {
if (!performSwingThreadRequiredWork(functionGraph, monitor)) {
return null;// shouldn't happen
}
@ -214,20 +214,22 @@ public class FunctionGraphFactory {
"\" (try another layout)";
}
private static boolean performSwingThreadRequiredWork(FunctionGraph functionGraph) {
final Collection<FGVertex> vertices = functionGraph.getVertices();
try {
SystemUtilities.runSwingNow(() -> {
private static boolean performSwingThreadRequiredWork(FunctionGraph functionGraph,
TaskMonitor monitor) throws CancelledException {
Collection<FGVertex> vertices = functionGraph.getVertices();
monitor.initialize(vertices.size(), "Building vertex components");
for (FGVertex v : vertices) {
v.getComponent();
}
});
return true;
monitor.increment();
try {
Swing.runNow(v::getComponent);
}
catch (Exception e) {
return false;
}
}
return true;
}
private static boolean isEntry(CodeBlock codeBlock) {
boolean isSource = true;

View file

@ -1,35 +0,0 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout;
import javax.swing.Icon;
import generic.theme.GIcon;
public abstract class ExperimentalLayoutProvider extends FGLayoutProviderExtensionPoint {
private static final Icon ICON = new GIcon("icon.plugin.functiongraph.layout.experimental");
@Override
public Icon getActionIcon() {
return ICON;
}
@Override
public int getPriorityLevel() {
return -100; // below the others
}
}

View file

@ -0,0 +1,245 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.function.Function;
import ghidra.graph.*;
import ghidra.graph.graphs.DefaultVisualGraph;
import ghidra.graph.viewer.VisualEdge;
import ghidra.graph.viewer.VisualVertex;
import ghidra.graph.viewer.layout.*;
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
import ghidra.util.exception.CancelledException;
/**
* Base class for graph layouts that layout a graph based on a tree structure with orthogonal edge
* routing.
* <P>
* The basic algorithm is to convert the graph to a tree and then, working bottom up in the tree,
* assign each vertex to a {@link GridLocationMap}. This is done by assigning each leaf
* vertex in its own simple 1x1 grid and them merging grids for sibling children side by side as
* you work up the tree. Merging sub-tree grids is done by shifting child grids to the right as
* each child grid is merged into the first (left most) child grid. The amount to shift a grid
* before it is merged is done by comparing the maximum column values for each row from the left
* grid to the minimum column values for each row from the right grid and finding the minimum shift
* need such that no rows overlap. This way, grids are stitched together sort of like a jig-saw
* puzzle.
* <P>
* Once the vertices are place in the grid, edge articulations are computed so that edges are
* routed orthogonally using an {@link OrthogonalEdgeRouter}.
* <P>
*
* To position the vertices and edges in layout space, it uses an
* {@link OrthogonalGridToLayoutMapper} to size the grid and map grid points to layout points.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public abstract class AbstractFlowChartLayout<V extends VisualVertex, E extends VisualEdge<V>>
extends AbstractVisualGraphLayout<V, E> {
protected Comparator<E> edgeComparator;
protected boolean leftAligned;
protected AbstractFlowChartLayout(DefaultVisualGraph<V, E> graph,
Comparator<E> edgeComparator, boolean leftAligned) {
super(graph, "Flow Chart");
this.edgeComparator = edgeComparator;
this.leftAligned = leftAligned;
}
@Override
protected GridLocationMap<V, E> performInitialGridLayout(VisualGraph<V, E> g)
throws CancelledException {
V root = getRoot(g);
GDirectedGraph<V, E> tree = GraphAlgorithms.toTree(g, root, edgeComparator);
GridLocationMap<V, E> grid = computeGridLocationMap(g, tree, root);
OrthogonalEdgeRouter<V, E> router = new OrthogonalEdgeRouter<>(grid);
router.setColumnExclusionFunction(e -> getExcludedCols(grid, tree, e));
router.computeAndSetEdgeArticulations(g.getEdges());
return grid;
}
@Override
protected LayoutPositions<V, E> positionInLayoutSpaceFromGrid(VisualGraph<V, E> g,
GridLocationMap<V, E> grid) throws CancelledException {
boolean isCondensed = isCondensedLayout();
Function<V, Shape> transformer = new VisualGraphVertexShapeTransformer<>();
OrthogonalGridToLayoutMapper<V, E> layoutMap =
new OrthogonalGridToLayoutMapper<V, E>(grid, transformer, isCondensed);
Map<V, Point2D> vertexMap = layoutMap.getVertexLocations();
Map<E, List<Point2D>> edgeMap = layoutMap.getEdgeLocations(vertexMap);
LayoutPositions<V, E> positions = LayoutPositions.createNewPositions(vertexMap, edgeMap);
// DEGUG triggers grid lines to be printed; useful for debugging
// VisualGraphRenderer.setGridPainter(new GridPainter(layoutMap.getGridCoordinates()));
layoutMap.dispose();
return positions;
}
protected abstract V getRoot(VisualGraph<V, E> g);
@Override
public boolean usesEdgeArticulations() {
return true;
}
@Override
protected Point2D getVertexLocation(V v, Column<V> col, Row<V> row,
Rectangle bounds) {
return getCenteredVertexLocation(v, col, row, bounds);
}
/**
* Creates a GridLocationMap for the subtree rooted at the given vertex. It does this by
* recursively getting grid maps for each of its children and then merging them together
* side by side.
* @param g the original graph
* @param tree the graph after edge removal to convert it into a tree
* @param v the root of the subtree to get a grid map for
* @return a GridLocationMap with the given vertex and all of its children position in the
* grid.
*/
private GridLocationMap<V, E> computeGridLocationMap(GDirectedGraph<V, E> g,
GDirectedGraph<V, E> tree, V v) {
Collection<E> edges = tree.getOutEdges(v);
if (edges.isEmpty()) {
GridLocationMap<V, E> grid = new GridLocationMap<>(v, 1, 1);
return grid;
}
// get all child grids and merge them side by side
List<E> sortedEdges = new ArrayList<>(edges);
sortedEdges.sort(edgeComparator);
E edge = sortedEdges.get(0);
V child = edge.getEnd();
int totalEdges = sortedEdges.size();
GridLocationMap<V, E> childGrid = computeGridLocationMap(g, tree, child);
int leftChildRootCol = childGrid.getRootColumn();
int rightChildRootCol = leftChildRootCol;
for (int i = 1; i < totalEdges; i++) {
edge = sortedEdges.get(i);
child = edge.getEnd();
GridLocationMap<V, E> nextGrid = computeGridLocationMap(g, tree, child);
int shift = merge(childGrid, nextGrid, i, totalEdges);
rightChildRootCol = nextGrid.getRootColumn() + shift;
}
int rootCol = (leftChildRootCol + rightChildRootCol) / 2;
if (leftAligned) {
rootCol = 1;
}
GridLocationMap<V, E> grid = new GridLocationMap<>(v, 1, rootCol);
grid.add(childGrid, 2, 0); // move child grid down 2: 1 for new root, 1 for edge row
return grid;
}
private int merge(GridLocationMap<V, E> leftGrid, GridLocationMap<V, E> rightGrid, int i,
int totalEdges) {
GridRange[] ranges = leftGrid.getVertexColumnRanges();
GridRange[] otherRanges = rightGrid.getVertexColumnRanges();
int shift = computeShift(ranges, otherRanges);
leftGrid.add(rightGrid, 0, shift);
return shift;
}
private int computeShift(GridRange[] ranges, GridRange[] otherRanges) {
int shift = 0;
int commonHeight = Math.min(ranges.length, otherRanges.length);
for (int i = 0; i < commonHeight; i++) {
GridRange range = ranges[i];
GridRange otherRange = otherRanges[i];
int myMax = range.max;
int otherMin = otherRange.min;
if (myMax >= otherMin - 1) {
int diff = myMax - otherMin + 2; // we want 1 empty column between
shift = Math.max(shift, diff);
}
}
return shift;
}
@SuppressWarnings("unchecked")
@Override
public VisualGraph<V, E> getVisualGraph() {
return (VisualGraph<V, E>) getGraph();
}
/**
* Returns a range of columns that we don't want to attempt to perform column routing through.
* Specifically, this is for back edges where we don't want them to route through columns that
* cut through any of its parents sub trees. This will force the routing algorithm to route
* around a nodes containing sub-tree instead of through it.
* @param grid the grid map that will be examined to find a routing column that doesn't
* have any blocking vertices.
* @param tree the tree version of the original graph
* @param e the edge to examine to find its parent's subtree column bounds
* @return a minimum and maximum column index through which the back edge should not be routed
*/
private GridRange getExcludedCols(GridLocationMap<V, E> grid, GDirectedGraph<V, E> tree, E e) {
GridRange range = new GridRange();
V v = e.getStart();
V ancestor = e.getEnd();
boolean isBackEdge = grid.row(v) >= grid.row(ancestor);
if (!isBackEdge) {
// no exclusions
return new GridRange();
}
V parent = getParent(tree, v);
while (parent != null) {
Collection<V> children = tree.getSuccessors(parent);
for (V child : children) {
range.add(grid.col(child));
}
parent = getParent(tree, parent);
if (parent == ancestor) {
break;
}
}
return range;
}
private V getParent(GDirectedGraph<V, E> tree, V v) {
Collection<V> predecessors = tree.getPredecessors(v);
if (predecessors == null || predecessors.isEmpty()) {
return null;
}
return predecessors.iterator().next();
}
}

View file

@ -0,0 +1,238 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.*;
import ghidra.graph.viewer.layout.GridPoint;
/**
* Maintains a collection of ColumnSegments for the same grid column.
*
* @param <E> edge type
*/
public class ColSegmentList<E> {
private List<ColumnSegment<E>> edgeSegments = new ArrayList<>();
private int col;
private long minY = Integer.MAX_VALUE;
private long maxY = Integer.MIN_VALUE;
public ColSegmentList(int col) {
this.col = col;
}
public int getCol() {
return col;
}
public ColSegmentList(ColumnSegment<E> segment) {
super();
addSegment(segment);
this.col = 0;
}
/**
* Assigns offsets for overlapping column segments. Parallel overlapping edges must be offset
* from each other when assigned to layout space to avoid drawing over each other. Each
* edge offset represents 1/2 the edge spacing distance. The reason offsets are assigned 2
* apart from each other is so that even numbers of columns can be centered. So for example,
* a column with 3 parallel edges are assigned offsets of -2,0,2, but 4 edges would be assigned
* -3,-1, 1, 3. (offset in each direction by 1/2 of an edge spacing)
*/
public void assignOffsets() {
// First partition the column segments into non-overlapping groups. Since column segments
// may attach to vertices, it is easier to center them on the vertices if not trying to
// consider all the segments in a column at the same time.
List<ColSegmentList<E>> groups = sortIntoNonOverlappingGroups(edgeSegments);
for (ColSegmentList<E> group : groups) {
assignOffsets(group);
}
}
/**
* Checks if the the range of y values in a given column segment list intersects the
* range of y values in this column segment.
* @param other the column segment list to compare
* @return true if they intersect ranges.
*/
boolean intersects(ColSegmentList<E> other) {
if (minY > other.maxY) {
return false;
}
if (other.minY > maxY) {
return false;
}
return true;
}
ColumnSegment<E> getSegment(E edge, GridPoint startPoint) {
for (ColumnSegment<E> edgeSegment : edgeSegments) {
if (edgeSegment.edge.equals(edge) && edgeSegment.startsAt(startPoint)) {
return edgeSegment;
}
}
return null;
}
@Override
public String toString() {
return edgeSegments.toString();
}
int getMinOffset() {
int minOffset = 0;
for (EdgeSegment<E> edgeSegment : edgeSegments) {
minOffset = Math.min(minOffset, edgeSegment.getOffset());
}
return minOffset;
}
int getMaxOffset() {
int maxOffset = 0;
for (EdgeSegment<E> edgeSegment : edgeSegments) {
maxOffset = Math.max(maxOffset, edgeSegment.getOffset());
}
return maxOffset;
}
void addSegment(ColumnSegment<E> segment) {
edgeSegments.add(segment);
minY = Math.min(minY, segment.getVirtualMinY());
maxY = Math.max(maxY, segment.getVirtualMaxY());
}
private void assignOffsets(ColSegmentList<E> group) {
// First sort the column segments in a left to right ordering.
Collections.sort(group.edgeSegments);
// Column segments are extend both to the right and left of the grid line, so first
// find a starting edge to assign to 0 and then work in both directions giving offsets
// to columns to avoid overlaps.
// see if there is a natural center line (from a vertex to vertex in one straight line)
int naturalCenter = findNaturalCenter(group);
int centerIndex = naturalCenter >= 0 ? naturalCenter : group.edgeSegments.size() / 2;
assignOffsets(group, centerIndex);
// if used an arbitrary center index, our edges might not be centered around
// the grid line, so adjust the offsets so the are.
if (naturalCenter < 0) {
int bias = group.getMaxOffset() + group.getMinOffset();
int adjustment = -bias / 2;
for (EdgeSegment<E> segment : group.edgeSegments) {
segment.setOffset(segment.getOffset() + adjustment);
}
}
}
private void assignOffsets(ColSegmentList<E> group, int center) {
List<ColSegmentList<E>> nonOverlappingSegments = new ArrayList<>();
// assign negative offsets to column segments to the left of the center segment.
for (int i = center; i >= 0; i--) {
ColumnSegment<E> segment = group.edgeSegments.get(i);
assignOffsets(nonOverlappingSegments, segment, -2); // 2 to keep edges two offsets apart
}
// remove all the previous results except for the columm as we still need to check
// for overlap against columns that have been assigned offset 0
for (int i = nonOverlappingSegments.size() - 1; i > 0; i--) {
nonOverlappingSegments.remove(i);
}
// assign positive offsets to column segments to the right of the center segment.
for (int i = center + 1; i < group.edgeSegments.size(); i++) {
ColumnSegment<E> segment = group.edgeSegments.get(i);
assignOffsets(nonOverlappingSegments, segment, 2); // 2 to keep edges two offsets apart
}
}
private void assignOffsets(List<ColSegmentList<E>> nonOverlappingSegments,
ColumnSegment<E> segment, int stepSize) {
// Find lowest offset group that the given segment can be added without an overlap.
// Start looking at the group with highest offsets first and work towards the 0
// offset group to ensure that overlapping segments don't lose the ordering that
// has already been establish. In other words, for a segment to be allowed to be
// given a 0 offset (because it doesn't overlap any segments in that group), it must
// also not overlap any existing groups with higher offsets. (Otherwise the ordering
// we created to minimize edge crossings will be lost)
int i = nonOverlappingSegments.size() - 1;
for (; i >= 0; i--) {
if (nonOverlappingSegments.get(i).hasOverlappingSegment(segment)) {
break;
}
}
// we either broke at a group we overlap or we are at -1. Either way, we get added
// to the next offset group.
i++;
if (i >= nonOverlappingSegments.size()) {
nonOverlappingSegments.add(new ColSegmentList<E>(i));
}
// if adjusting to the left, offsets are negative
int offset = i * stepSize;
segment.setOffset(offset);
nonOverlappingSegments.get(i).addSegment(segment);
}
private boolean hasOverlappingSegment(ColumnSegment<E> segment) {
for (ColumnSegment<E> edgeSegment : edgeSegments) {
if (segment.overlaps(edgeSegment)) {
return true;
}
}
return false;
}
private int findNaturalCenter(ColSegmentList<E> group) {
for (int i = 0; i < group.edgeSegments.size(); i++) {
ColumnSegment<E> edgeSegment = group.edgeSegments.get(i);
if (edgeSegment.points.size() == 2) {
return i;
}
}
return -1;
}
private List<ColSegmentList<E>> sortIntoNonOverlappingGroups(List<ColumnSegment<E>> segments) {
List<ColSegmentList<E>> groups = new ArrayList<>(segments.size());
for (ColumnSegment<E> segment : segments) {
groupSegment(groups, segment);
}
return groups;
}
private void groupSegment(List<ColSegmentList<E>> groups, ColumnSegment<E> segment) {
ColSegmentList<E> newGroup = new ColSegmentList<E>(segment);
for (int i = groups.size() - 1; i >= 0; i--) {
if (newGroup.intersects(groups.get(i))) {
newGroup.merge(groups.get(i));
groups.remove(i);
}
}
groups.add(newGroup);
}
private void merge(ColSegmentList<E> segmentList) {
edgeSegments.addAll(segmentList.edgeSegments);
minY = Math.min(minY, segmentList.minY);
maxY = Math.max(maxY, segmentList.maxY);
}
}

View file

@ -0,0 +1,346 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.List;
import ghidra.graph.viewer.layout.GridPoint;
/**
* Vertical edge segments of an edge with articulation points. Each pair of points in the list
* of articulation points corresponds to either a column segment or a row segment. There is
* a built-in assumption in the sorting algorithm that the list of articulation points always
* start and end with a column segment. See {@link EdgeSegment} for more information.
*
* @param <E> The edge type
*/
public class ColumnSegment<E> extends EdgeSegment<E> implements Comparable<ColumnSegment<E>> {
// specifies the orientation of the row attached to this segment (either top or bottom)
enum RowOrientation {
LEFT, // the attached row extends to the left of this column
TERMINAL, // there is no attached row since this segment ends at a vertex.
RIGHT // the attached row extends to the right of this column
}
private RowSegment<E> next;
private RowSegment<E> previous;
/**
* Constructs the first segment which is always a column segment. This constructor will
* also create all the follow-on segments which can be retrieved via the {@link #nextSegment()}
* method.
* @param e the edge to create segments for
* @param points the articulation points for the edge.
*/
public ColumnSegment(E e, List<GridPoint> points) {
this(null, e, points, 0);
}
/**
* Package method for creating the column segments at a specific pointIndex.
* @param previous the row segment the precedes this column segment
* @param e the edge this segment is for
* @param points the list of articulation points for the edge
* @param pointIndex the index into the points list that is the first point for this segment
*/
ColumnSegment(RowSegment<E> previous, E e, List<GridPoint> points, int pointIndex) {
super(e, points, pointIndex);
this.previous = previous;
if (pointIndex < points.size() - 2) {
next = new RowSegment<E>(this, e, points, pointIndex + 1);
}
}
/**
* Returns the grid column index this column segment.
* @return the grid column index this column segment
*/
public int getCol() {
return points.get(pointIndex).col;
}
/**
* Return the index of the row where this column segment starts. Note that this is different
* from the top row. The start row is in the order of the articulation points whereas the top
* row is always the spatially upper (lower row index) row of either the start row or end row.
* @return the index of the grid row for the start point of this segment
*/
public int getStartRow() {
return points.get(pointIndex).row;
}
/**
* Return the index of the row where this column segment ends. Note that this is different
* from the bottom row. The end row is in the order of the articulation points. The bottom row
* is always the spatially lower (higher row index) row of either the start row or end row.
* @return the index of the grid row for the end point of this segment
*/
public int getEndRow() {
return points.get(pointIndex + 1).row;
}
@Override
public int compareTo(ColumnSegment<E> other) {
// To make the comparison reversible and transitive, it is important that we are
// consistent in the order we compare segments. We arbitrarily chose to always compare
// the order by following the shape of the top and only considering the shape on the bottom
// if the tops are equal.
//
// NOTE: Segments are compared by following the next or previous segments until one of the
// segments definitively determines the order. When comparing segments in a particular
// direction, is is important not to directly call the compareTo methods as that could result
// in an infinite loop. Instead, when comparing in a particular direction, just directly
// use the appropriate direction comparison so that it will follow that direction until
// it finds a difference or it simply returns 0, in which case the original to
// compareTo can then try the other direction. As a consequence of this, the basic obvious
// comparison of the grid columns first had to be moved into both the compareTops and the
// compareBottoms.
int result = compareTops(other);
if (result == 0) {
result = compareBottoms(other);
}
return result;
}
/**
* Compares column segments strictly based on the relationship of the connected rows at the top
* of the segments.
* @param other the other column segment to compare
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*/
public int compareTops(ColumnSegment<E> other) {
// first, just check the columns of this segment and the other, if the columns are
// not the same, then one column clearly is to the left of the other.
int result = getCol() - other.getCol();
if (result != 0) {
return result;
}
// We are in the same grid column,so the order is determined by the segment at the
// top of the column. There are 3 possible orientations for the top row. 1) it can
// extend to the left of our column, it can be a terminal point, or it can extend to the
// right. If our orientation is not the same as the other column segment, the order is
// simply LEFT < TERMINAL < RIGHT in order to reduce edge crossings. Edges on the left
// go left and edges on the right go right, so they won't cross each other.
RowOrientation myTopRowOrientation = getOrientationForTopRow();
RowOrientation otherTopRowOrientation = other.getOrientationForTopRow();
// if they are not the same, then LEFT < TERMINAL < RIGHT
if (myTopRowOrientation != otherTopRowOrientation) {
return myTopRowOrientation.compareTo(otherTopRowOrientation);
}
// Both this segment and the other segment have the same orientation. Compare the top
// row segments and use those results to determine our ordering left to right. If the
// top rows extend to the left, then which ever row is above (lower value), should have
// its associated column be to the right (higher value). Keeping lower rows (higher value)
// on the left allows their shape to avoid being crossed by the taller shape, which will be
// on the right.
//
// And if the top rows extends the right, the inverse applies.
switch (myTopRowOrientation) {
case LEFT:
RowSegment<E> myTopRowSegment = getTopRowSegment();
RowSegment<E> otherTopRowSegment = other.getTopRowSegment();
return -myTopRowSegment.compareLefts(otherTopRowSegment);
case RIGHT:
myTopRowSegment = getTopRowSegment();
otherTopRowSegment = other.getTopRowSegment();
return myTopRowSegment.compareRights(otherTopRowSegment);
case TERMINAL:
default:
return 0;
}
}
/**
* Compares column segments strictly based on the relationship of the connected rows at the
* bottom of the segments.
* @param other the other column segment to compare
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*/
public int compareBottoms(ColumnSegment<E> other) {
// first, just check the columns of this segment and the other, if the columns are
// not the same, then one column clearly is to the left of the other.
int result = getCol() - other.getCol();
if (result != 0) {
return result;
}
// We are in the same grid column, the order is determined by the segment at the
// bottom of the column (we already tried the top and they were equal). There are
// 3 possible orientations for the botom row. 1) it can
// extend to the left of our column, it can be a terminal point, or it can extend to the
// right. If our orientation is not the same as the other column segment, the order is
// simply LEFT < TERMINAL < RIGHT in order to reduce edge crossings. Edges on the left
// go left and edges on the right go right, so they won't cross each other.
RowOrientation myBottomRowOrientation = getOrientationForBottomRow();
RowOrientation otherTopRowOrientation = other.getOrientationForBottomRow();
// if they are not the same, then LEFT < TERMINAL < RIGHT
if (myBottomRowOrientation != otherTopRowOrientation) {
return myBottomRowOrientation.compareTo(otherTopRowOrientation);
}
// Both this segment and the other segment have the same orientation. Compare the bottom
// row segments and use those results to determine our ordering left to right. If the
// bottom rows extend to the left, then which ever row is above (lower value), should have
// its associated column be to the left (lower value). Keeping lower rows (higher value)
// on the right allows their shape to avoid being crossed by the taller shape, which will be
// on the left.
//
// And if the top rows extends the right, the inverse applies.
switch (myBottomRowOrientation) {
case LEFT:
RowSegment<E> myBottomRowSegment = getBottomRowSegment();
RowSegment<E> otherBottomRowSegment = other.getBottomRowSegment();
return myBottomRowSegment.compareLefts(otherBottomRowSegment);
case RIGHT:
myBottomRowSegment = getBottomRowSegment();
otherBottomRowSegment = other.getBottomRowSegment();
return -myBottomRowSegment.compareRights(otherBottomRowSegment);
case TERMINAL:
default:
return 0;
}
}
/**
* Checks if the given column segments overlaps vertically.
* @param other the other column segment to compare
* @return true if these would overlap if drawn with same x column coordinate
*/
public boolean overlaps(ColumnSegment<E> other) {
if (getVirtualMinY() > other.getVirtualMaxY()) {
return false;
}
if (getVirtualMaxY() < other.getVirtualMinY()) {
return false;
}
return true;
}
@Override
public RowSegment<E> nextSegment() {
return next;
}
@Override
public RowSegment<E> previousSegment() {
return previous;
}
/**
* Returns true if this is the first segment in an edge articulation point list.
* @return true if this is the first segment in an edge articulation point list
*/
public boolean isStartSegment() {
return previous == null;
}
/**
* Returns true if this is the last segment in an edge articulation point list.
* @return true if this is the last segment in an edge articulation point list
*/
public boolean isEndSegment() {
return next == null;
}
/**
* Returns a top y position assuming rows are one million pixels apart for comparison purposes.
* It takes into account the assigned offsets of the attached rows. This method depends on
* row offsets having already been assigned.
* @return a virtual top y position only useful for comparison purposes.
*/
public int getVirtualMinY() {
return Math.min(getVirtualStartY(), getVirtualEndY());
}
/**
* Returns a bottom y position assuming rows are one million pixels apart for comparison
* purposes. It takes into account the assigned offsets of the attached rows. This method
* depends on row offsets having already been assigned.
* @return a virtual bottom y position only useful for comparison purposes.
*/
public int getVirtualMaxY() {
return Math.max(getVirtualStartY(), getVirtualEndY());
}
private int getVirtualStartY() {
// start segments are given a slight downward offset for comparison purposes to
// avoid being overlapped by ends segments that end on the same vertex as we begin
int offset = previous == null ? 1 : previous.getOffset();
return getStartRow() * 1000000 + offset;
}
private int getVirtualEndY() {
// end segments are given a slight upward offset for comparison purposes to
// avoid being overlapped by ends segments that start on the same vertex as we end
int offset = next == null ? -1 : next.getOffset();
return getEndRow() * 1000000 + offset;
}
private RowSegment<E> getTopRowSegment() {
return flowsUp() ? next : previous;
}
private RowSegment<E> getBottomRowSegment() {
return flowsUp() ? previous : next;
}
private RowOrientation getOrientationForTopRow() {
if (isStartSegment()) {
return RowOrientation.TERMINAL;
}
RowSegment<E> topRowSegment = getTopRowSegment();
int topRowOtherCol = flowsUp() ? topRowSegment.getEndCol() : topRowSegment.getStartCol();
return topRowOtherCol < getCol() ? RowOrientation.LEFT : RowOrientation.RIGHT;
}
private RowOrientation getOrientationForBottomRow() {
if (isEndSegment()) {
return RowOrientation.TERMINAL;
}
RowSegment<E> bottomRowSegment = getBottomRowSegment();
int bottomRowOtherCol =
flowsUp() ? bottomRowSegment.getStartCol() : bottomRowSegment.getEndCol();
return bottomRowOtherCol < getCol() ? RowOrientation.LEFT : RowOrientation.RIGHT;
}
private boolean flowsUp() {
return getStartRow() > getEndRow();
}
public ColumnSegment<E> last() {
if (isEndSegment()) {
return this;
}
return next.last();
}
}

View file

@ -0,0 +1,112 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.List;
import ghidra.graph.viewer.layout.GridPoint;
/**
* Base class for edge segments that are part of an articulated edge. Basically, edge articulations
* are stored as a list of {@link GridPoint}s while in grid space. Each pair of points in the list
* of points represents either a row or column segment. These segments are useful for orthogonal
* edge routing algorithms as they provide a higher level API instead of dealing directly with
* the points list.
* <P>
* Each segment has its related edge object and the full list of articulation points so that can
* also provide information on its connected segments. The point index is simply the index into
* the points list of the first point in the segment that this segment object represents.
* <P>
* Segments also maintain a linked list to the other segments that make up the edge which can
* be retrieved via the {@link #nextSegment()} and {@link #previousSegment()} methods respectively.
*
* @param <E> the edge type
*/
public abstract class EdgeSegment<E> {
protected E edge;
protected List<GridPoint> points; // this is a list of all articulations points for the edge
protected int pointIndex; // the index into the points of the first point in the segment
private int offset; // holds any offset assigned to this segment by edge routing
public EdgeSegment(E e, List<GridPoint> points, int pointIndex) {
this.edge = e;
this.points = points;
this.pointIndex = pointIndex;
}
public E getEdge() {
return edge;
}
/**
* Sets the offset from the grid line for this segment. Edge routing algorithms will set
* this value to keep overlapping segments in the same row or column for being assigned to
* the same exact layout space location.
* @param offset the distance from the grid line to use when assigning to layout space
*/
public void setOffset(int offset) {
this.offset = offset;
}
/**
* The amount of x or y space to use when assigning to layout space to prevent this segment
* from overlapping segments from other edges.
* @return the offset from the grid line.
*/
public int getOffset() {
return offset;
}
@Override
public String toString() {
return String.format("%s, i = %d, offset = %s", edge, pointIndex, offset);
}
/**
* Returns true if this edge ends at or above its start row.
* @return true if this edge ends at or above its start row
*/
public boolean isBackEdge() {
GridPoint start = points.get(0);
GridPoint end = points.get(points.size() - 1);
return start.row >= end.row;
}
/**
* Returns true if this segment starts at the given point.
* @param p the grid point to check
* @return true if this segment starts at the given point
*/
public boolean startsAt(GridPoint p) {
return points.get(pointIndex).equals(p);
}
/**
* Returns the next edge segment after this one or null if this is the last segment. If the
* this segment is a RowSegment, the next segment will be a ColumnSegment and vise-versa.
* @return the next edge segment.
*/
public abstract EdgeSegment<E> nextSegment();
/**
* Returns the previous edge segment before this one or null if this is the first segment. If
* the this segment is a RowSegment, the previous segment will be a ColumnSegment and
* vise-versa.
* @return the previous edge segment.
*/
public abstract EdgeSegment<E> previousSegment();
}

View file

@ -0,0 +1,118 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.*;
import ghidra.graph.viewer.layout.GridLocationMap;
import ghidra.graph.viewer.layout.GridPoint;
/**
* Organizes all edge segments from a {@link GridLocationMap} into rows and column objects and
* then assigns offsets to overlapping segments within a row or column. Offsets are values that
* represent either x or y distances that will later be added to a row segment's y coordinate or a
* column segment's x coordinate to keep them from overlapping in layout space.
* <P>
* The offsets have to be computed before sizing the grid because the offsets affect
* the size of the grid rows and columns.
*
* @param <E> the edge type
*/
public class EdgeSegmentMap<E> {
private Map<Integer, RowSegmentList<E>> rowSegmentMap = new HashMap<>();
private Map<Integer, ColSegmentList<E>> colSegmentMap = new HashMap<>();
public EdgeSegmentMap(GridLocationMap<?, E> grid) {
createEdgeSegments(grid);
assignEdgeSegmentOffsets();
}
/**
* Returns a collection of all edge row segment lists.
* @return a collection of all edge row segment lists
*/
public Collection<RowSegmentList<E>> rowSegments() {
return rowSegmentMap.values();
}
/**
* Returns a collection of all edge column segment lists.
* @return a collection of all edge column segment lists
*/
public Collection<ColSegmentList<E>> colSegments() {
return colSegmentMap.values();
}
/**
* Finds the column segment object for the given edge and start point.
* @param edge the edge for which to find its column segment object
* @param gridPoint the start point for the desired segment object
* @return the column segment object for the given edge and start point.
*/
public ColumnSegment<E> getColumnSegment(E edge, GridPoint gridPoint) {
ColSegmentList<E> colSegments = colSegmentMap.get(gridPoint.col);
return colSegments == null ? null : colSegments.getSegment(edge, gridPoint);
}
public void dispose() {
rowSegmentMap.clear();
colSegmentMap.clear();
}
private void createEdgeSegments(GridLocationMap<?, E> grid) {
for (E edge : grid.edges()) {
List<GridPoint> gridPoints = grid.getArticulations(edge);
ColumnSegment<E> colSegment = new ColumnSegment<E>(edge, gridPoints);
addColSegment(colSegment);
// segments always start and end with a column segment, so any additional segments
// will be in pairs of a row segment followed by a column segment
while (!colSegment.isEndSegment()) {
RowSegment<E> rowSegment = colSegment.nextSegment();
addRowSegment(rowSegment);
colSegment = rowSegment.nextSegment();
addColSegment(colSegment);
}
}
}
private void assignEdgeSegmentOffsets() {
for (RowSegmentList<E> rowSegments : rowSegmentMap.values()) {
rowSegments.assignOffsets();
}
for (ColSegmentList<E> colSegments : colSegmentMap.values()) {
colSegments.assignOffsets();
}
}
private void addRowSegment(RowSegment<E> rowSegment) {
int row = rowSegment.getRow();
RowSegmentList<E> edgeRow =
rowSegmentMap.computeIfAbsent(row, k -> new RowSegmentList<E>(k));
edgeRow.addSegment(rowSegment);
}
private void addColSegment(ColumnSegment<E> colSegment) {
int col = colSegment.getCol();
ColSegmentList<E> edgeCol =
colSegmentMap.computeIfAbsent(col, k -> new ColSegmentList<E>(k));
edgeCol.addSegment(colSegment);
}
}

View file

@ -0,0 +1,98 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.Comparator;
import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer;
import ghidra.app.plugin.core.functiongraph.graph.FGEdge;
import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph;
import ghidra.app.plugin.core.functiongraph.graph.jung.renderer.FGEdgeRenderer;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayout;
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
import ghidra.app.plugin.core.functiongraph.mvc.FunctionGraphOptions;
import ghidra.graph.VisualGraph;
import ghidra.graph.viewer.layout.AbstractVisualGraphLayout;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.program.model.symbol.FlowType;
import ghidra.program.model.symbol.RefType;
/**
* Adapts the {@link AbstractFlowChartLayout} to work for {@link FunctionGraph}s.
*/
public class FGFlowChartLayout extends AbstractFlowChartLayout<FGVertex, FGEdge>
implements FGLayout {
private FunctionGraphOptions options;
protected FGFlowChartLayout(FunctionGraph graph, boolean leftAligned) {
super(graph, new FGEdgeComparator(), leftAligned);
this.options = graph.getOptions();
}
@Override
public FunctionGraph getVisualGraph() {
return (FunctionGraph) super.getVisualGraph();
}
@Override
public AbstractVisualGraphLayout<FGVertex, FGEdge> createClonedLayout(
VisualGraph<FGVertex, FGEdge> newGraph) {
return new FGFlowChartLayout((FunctionGraph) newGraph, leftAligned);
}
@Override
public FGLayout cloneLayout(VisualGraph<FGVertex, FGEdge> newGraph) {
VisualGraphLayout<FGVertex, FGEdge> clone = super.cloneLayout(newGraph);
return (FGLayout) clone;
}
@Override
protected boolean isCondensedLayout() {
return options.useCondensedLayout();
}
@Override
public BasicEdgeRenderer<FGVertex, FGEdge> getEdgeRenderer() {
return new FGEdgeRenderer();
}
private static class FGEdgeComparator implements Comparator<FGEdge> {
@Override
public int compare(FGEdge e1, FGEdge e2) {
return priority(e1).compareTo(priority(e2));
}
private Integer priority(FGEdge e) {
FlowType type = e.getFlowType();
// making fall through edges a higher priority, makes it more likely that vertices
// with fall through connections will be direct descendants (closer) when the graph is
// converted to a tree.
if (type == RefType.FALL_THROUGH) {
return 1; // lower is more preferred
}
return 10;
}
}
@Override
protected FGVertex getRoot(VisualGraph<FGVertex, FGEdge> g) {
if (graph instanceof FunctionGraph fg) {
return fg.getRootVertex();
}
return null;
}
}

View file

@ -0,0 +1,54 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import javax.swing.Icon;
import generic.theme.GIcon;
import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayout;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutProviderExtensionPoint;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Makes the Flow Chart Layout available for the function graph feature.
*/
public class FlowChartLayoutProvider extends FGLayoutProviderExtensionPoint {
private static final Icon ICON = new GIcon("icon.plugin.functiongraph.layout.flowchart");
@Override
public String getLayoutName() {
return "Flow Chart";
}
@Override
public Icon getActionIcon() {
return ICON;
}
@Override
public int getPriorityLevel() {
return 140;
}
@Override
public FGLayout getFGLayout(FunctionGraph graph, TaskMonitor monitor)
throws CancelledException {
return new FGFlowChartLayout(graph, false);
}
}

View file

@ -0,0 +1,54 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import javax.swing.Icon;
import generic.theme.GIcon;
import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayout;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutProviderExtensionPoint;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Makes the Left Aligned Flow Chart Layout available for the function graph feature.
*/
public class LeftAlignedFlowChartLayoutProvider extends FGLayoutProviderExtensionPoint {
private static final Icon ICON = new GIcon("icon.plugin.functiongraph.layout.flowchart.left");
@Override
public String getLayoutName() {
return "Flow Chart (Left)";
}
@Override
public Icon getActionIcon() {
return ICON;
}
@Override
public int getPriorityLevel() {
return 130;
}
@Override
public FGLayout getFGLayout(FunctionGraph graph, TaskMonitor monitor)
throws CancelledException {
return new FGFlowChartLayout(graph, true);
}
}

View file

@ -0,0 +1,173 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.*;
import java.util.function.Function;
import ghidra.graph.viewer.VisualEdge;
import ghidra.graph.viewer.VisualVertex;
import ghidra.graph.viewer.layout.*;
/**
* Routes edges orthogonally for vertices positioned in a {@link GridLocationMap}.
* This algorithm creates articulation points such that outgoing edges always exit the
* start vertex from the bottom and enter the end vertex from the top.
* <P>
* There are only three types of edges created by this algorithm. The first
* type is an edge with one segment that goes directly from a start vertex straight down to an
* end vertex.
* <P>
* The second type is an edge with three segments that goes down from the start vertex,
* goes left or right to the column of the end vertex and then goes down to a child vertex.
* <P>
* The third type consists of 5 segments and can connect any two vertices in the graph. It starts
* by going down to the next row from the start vertex, then goes left or right until it finds a
* column where there are no vertices between that row and the row above the end vertex. It then
* goes left or right in that row to the column of the end vertex and then down to that vertex.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class OrthogonalEdgeRouter<V extends VisualVertex, E extends VisualEdge<V>> {
private Function<E, GridRange> excludedColumnsFunction;
private GridLocationMap<V, E> grid;
private Map<Integer, Column<V>> columnMap;
public OrthogonalEdgeRouter(GridLocationMap<V, E> grid) {
this.grid = grid;
columnMap = grid.columnsMap();
excludedColumnsFunction = e -> new GridRange();
}
/**
* Computes a list of articulations points in grid space for each edge in the given collection
* and sets those points into the {@link GridLocationMap}.
* @param edges computes articulations points in grid space and sets them on the grid
*/
public void computeAndSetEdgeArticulations(Collection<E> edges) {
for (E e : edges) {
V v1 = e.getStart();
V v2 = e.getEnd();
GridPoint p1 = grid.gridPoint(v1);
GridPoint p2 = grid.gridPoint(v2);
int routingCol = findRoutingColumn(e, p1, p2);
List<GridPoint> edgePoints = getEdgePoints(p1, p2, routingCol);
grid.setArticulations(e, edgePoints);
}
}
/**
* Sets a function that can be used to prevent edges from being routed in a range of columns.
* One use of this is to prevent back edges from intersecting any child trees in its ancestor
* hierarchy between the start vertex and the end vertex.
* @param excludedColumnsFunction the function to call to compute a range of columns to
* prevent routing edges.
*/
public void setColumnExclusionFunction(Function<E, GridRange> excludedColumnsFunction) {
this.excludedColumnsFunction = excludedColumnsFunction;
}
private int findRoutingColumn(E e, GridPoint p1, GridPoint p2) {
if (p2.row == p1.row + 2) {
return p1.col; // direct child
}
int startRow = Math.min(p1.row + 1, p2.row - 1);
int endRow = Math.max(p1.row + 1, p2.row - 1);
int startCol = Math.min(p1.col, p2.col);
int endCol = Math.max(p1.col, p2.col);
boolean isBackEdge = p2.row <= p1.row;
// If not a back edge, try to route in between start and end columns, but we decided not
// to ever route back edges in between so that back edges have C-shape or backwards C-shape
// and not a Z-shape.
if (!isBackEdge) {
// try if either start or end column is open
if (isOpenPath(p1.col, startRow, endRow)) {
return p1.col;
}
if (isOpenPath(p2.col, startRow, endRow)) {
return p2.col;
}
for (int col = startCol + 1; col <= endCol - 1; col++) {
if (isOpenPath(col, startRow, endRow)) {
return col;
}
}
}
// Get an optional excluded range where we don't want to route a specific edge. By
// default the range is empty, allowing any column.
GridRange excludedRange = excludedColumnsFunction.apply(e);
// try each each left and right column expanding outwards, starting at the columns of
// the start and end vertex.
for (int i = 1; i <= startCol; i++) {
int left = startCol - i;
int right = endCol + i;
boolean leftExcluded = excludedRange.contains(left);
boolean rightExcluded = excludedRange.contains(right);
boolean leftValid = !leftExcluded && isOpenPath(left, startRow, endRow);
boolean rightValid = !rightExcluded && isOpenPath(right, startRow, endRow);
if (leftValid) {
if (!rightValid) {
return left;
}
// if both are open, prefer left for forward edges, right for back edges
return p1.row < p2.row ? left : right;
}
else if (rightValid) {
return right;
}
}
return 0; // 0 is always open as we avoid putting vertices in the 0 column
}
private boolean isOpenPath(int col, int startRow, int endRow) {
Column<V> column = columnMap.get(col);
if (column == null) {
return true;
}
return column.isOpenBetween(startRow, endRow);
}
private List<GridPoint> getEdgePoints(GridPoint p1, GridPoint p2, int routingCol) {
List<GridPoint> points = new ArrayList<GridPoint>();
points.add(p1);
if (routingCol == p1.col) {
if (routingCol != p2.col) {
points.add(new GridPoint(p2.row - 1, p1.col));
points.add(new GridPoint(p2.row - 1, p2.col));
}
}
else {
points.add(new GridPoint(p1.row + 1, p1.col));
points.add(new GridPoint(p1.row + 1, routingCol));
if (routingCol != p2.col) {
points.add(new GridPoint(p2.row - 1, routingCol));
points.add(new GridPoint(p2.row - 1, p2.col));
}
}
points.add(p2);
return points;
}
}

View file

@ -0,0 +1,181 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.awt.Rectangle;
import java.awt.Shape;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import ghidra.graph.viewer.layout.*;
/**
* Computes positions in layout space for {@link GridLocationMap}s that have orthogonally routed
* edges.
* <P>
* At this point, the grid has been populated with vertices at specific grid points and edges with
* lists of grid articulation points for their routing. Conceptually, vertices live at the
* intersection of the grid lines and edges are routed along grid lines. While in grid space,
* vertices have no size and edges can overlap each other along grid lines. In order to map these
* virtual grid locations to points in layout space, we need to use the size of each vertex and
* offsets assigned to parallel edge segments that share a grid line.
* <P>
* We first need to compute sizes for the rows and columns in the grid. For purposes of this
* algorithm, the size of a row N is defined to be the distance between grid line row N and grid
* line row N+1. The same applies to column sizes. The way vertex sizes are applied is slightly
* different for rows and column. Vertices are centered on grid column lines, but
* completely below grid row lines. So if a vertex is at grid point 1,1, all of its height is
* assigned to grid row 1. But its width is split between grid column 0 and grid column 1. Edges
* work similarly, in that parallel horizontal edge segments extend below their grid row, but
* parallel column segments are split so that some have offsets that are before the vertical
* grid line and some are after.
* <P>
* The row sizing is straight forward. Even rows only contain edges and odd rows only contain
* vertices. Since the height of a vertex is assigned completely to one row, that row's height
* is simply the maximum height of all the vertices in that row, plus any row padding.
* <P>
* Column sizing is more complicated. The width of any column is going to be the the max of either
* 1) vertices that half extend from the left + the thickness of edges that extend the right, OR
* 2) vertices that half extend from the right + the thickness of edges the extend from the left.
* Also, column padding is applied differently. For columns, padding is not just added to the
* column width like in rows. Instead, it acts as a minimum "edge thickness". In other words if the
* edge thickness is less than the padding, the edge thickness doesn't make the gaps bigger. Only if
* the edge thickness is greater the the column padding, then it determines the gap and the
* column padding contributes nothing.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class OrthogonalGridSizer<V, E> {
private static final int EDGE_ROW_PADDING = 25; // space before an edge row
private static final int VERTEX_ROW_PADDING = 35; // space before a vertex vow
private static final int COL_PADDING = 30; // minimum space for a column
private static final int CONDENSED_EDGE_ROW_PADDING = 15;
private static final int CONDENSED_VERTEX_ROW_PADDING = 25;
private static final int CONDENSED_COL_PADDING = 15;
private int[] rowHeights;
private int[] beforeColEdgeWidths;
private int[] afterColEdgeWidths;
private int[] colVertexWidths;
private int edgeSpacing;
public OrthogonalGridSizer(GridLocationMap<V, E> gridMap, EdgeSegmentMap<E> segmentMap,
Function<V, Shape> transformer, int edgeSpacing) {
this.edgeSpacing = edgeSpacing;
rowHeights = new int[gridMap.height()];
beforeColEdgeWidths = new int[gridMap.width()];
afterColEdgeWidths = new int[gridMap.width()];
colVertexWidths = new int[gridMap.width()];
addVertexSizes(gridMap, transformer);
addEdgeRowSizes(gridMap, segmentMap);
addEdgeColSizes(gridMap, segmentMap);
}
public GridCoordinates getGridCoordinates(boolean isCondensed) {
int[] rowStarts = new int[rowCount() + 1];
int[] colStarts = new int[colCount() + 1];
int vertexRowPadding = isCondensed ? CONDENSED_VERTEX_ROW_PADDING : VERTEX_ROW_PADDING;
int edgeRowPadding = isCondensed ? CONDENSED_EDGE_ROW_PADDING : EDGE_ROW_PADDING;
int colPadding = isCondensed ? CONDENSED_COL_PADDING : COL_PADDING;
for (int row = 1; row < rowStarts.length; row++) {
// edges rows are even, vertex rows are odd
int rowPadding = row % 2 == 0 ? edgeRowPadding : vertexRowPadding;
rowStarts[row] = rowStarts[row - 1] + height(row - 1, rowPadding);
}
for (int col = 1; col < colStarts.length; col++) {
colStarts[col] = colStarts[col - 1] + width(col - 1, colPadding);
}
return new GridCoordinates(rowStarts, colStarts);
}
private int rowCount() {
return rowHeights.length;
}
private int colCount() {
return colVertexWidths.length;
}
private int height(int row, int rowPadding) {
return rowHeights[row] + rowPadding;
}
private int width(int col, int minColPadding) {
int leftEdgeWidth = Math.max(afterColEdgeWidths[col], minColPadding);
int leftVertexWidth = colVertexWidths[col] / 2;
int rightEdgeWidth = 0;
int rightVertexWidth = 0;
if (col < colVertexWidths.length - 1) {
rightEdgeWidth = Math.max(beforeColEdgeWidths[col + 1], minColPadding);
rightVertexWidth = colVertexWidths[col + 1] / 2;
}
int width = Math.max(leftEdgeWidth + rightVertexWidth, rightEdgeWidth + leftVertexWidth);
width = Math.max(width, leftEdgeWidth + rightEdgeWidth);
return width;
}
private void addEdgeColSizes(GridLocationMap<V, E> gridMap, EdgeSegmentMap<E> segmentMap) {
for (ColSegmentList<E> colSegments : segmentMap.colSegments()) {
int col = colSegments.getCol();
int edgesToLeft = -colSegments.getMinOffset();
int edgesToRight = colSegments.getMaxOffset();
addColumnEdgeWidth(col, edgesToLeft * edgeSpacing, edgesToRight * edgeSpacing);
}
}
private void addEdgeRowSizes(GridLocationMap<V, E> gridMap, EdgeSegmentMap<E> segmentMap) {
// edge rows have no vertices, so its height just depends on the number of parallel edges
for (RowSegmentList<E> rowSegments : segmentMap.rowSegments()) {
int row = rowSegments.getRow();
int edgeCount = rowSegments.getMaxOffset() - rowSegments.getMinOffset();
addRowHeight(row, edgeCount * edgeSpacing);
}
}
private void addVertexSizes(GridLocationMap<V, E> gridMap, Function<V, Shape> transformer) {
Map<V, GridPoint> vertexPoints = gridMap.getVertexPoints();
for (Entry<V, GridPoint> entry : vertexPoints.entrySet()) {
V v = entry.getKey();
GridPoint p = entry.getValue();
Shape shape = transformer.apply(v);
Rectangle vertexBounds = shape.getBounds();
addRowHeight(p.row, vertexBounds.height);
addColumnVertexWidth(p.col, vertexBounds.width);
}
}
private void addRowHeight(int row, int height) {
rowHeights[row] = Math.max(rowHeights[row], height);
}
private void addColumnVertexWidth(int col, int width) {
colVertexWidths[col] = Math.max(colVertexWidths[col], width);
}
private void addColumnEdgeWidth(int col, int beforeWidth, int afterWidth) {
beforeColEdgeWidths[col] = Math.max(beforeColEdgeWidths[col], beforeWidth);
afterColEdgeWidths[col] = Math.max(afterColEdgeWidths[col], afterWidth);
}
}

View file

@ -0,0 +1,165 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import ghidra.graph.viewer.VisualEdge;
import ghidra.graph.viewer.VisualVertex;
import ghidra.graph.viewer.layout.*;
/**
* Maps the grid points of vertices and edges in a {@link GridLocationMap} to x,y locations in
* layout space. The mapping is specifically for grids that contain edges that have been given
* orthogonally routed articulation points.
* <P>
* First the edges have to be organized into row and column segments so that overlapping segments
* can be assigned offsets from the grid lines so that they won't overlap in layout space. Also,
* the offset information is need to provide "thickness" for the edges in a row or column.
* Next, the grid size and positioning in layout space is computed. Finally, it is a simple matter
* to translate grid points for vertices directly into points in layout space. Edge points are
* similar, but they need to add their assigned offset for each segment to its corresponding
* translated grid articulation point.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class OrthogonalGridToLayoutMapper<V extends VisualVertex, E extends VisualEdge<V>> {
private static final int EDGE_SPACING = 7;
private static final int CONDENSED_EDGE_SPACING = 5;
private EdgeSegmentMap<E> segmentMap;
private GridLocationMap<V, E> grid;
private Function<V, Shape> transformer;
private GridCoordinates gridCoordinates;
private int edgeSpacing;
public OrthogonalGridToLayoutMapper(GridLocationMap<V, E> grid, Function<V, Shape> transformer,
boolean isCondensed) {
this.grid = grid;
this.transformer = transformer;
this.edgeSpacing = isCondensed ? CONDENSED_EDGE_SPACING : EDGE_SPACING;
// organize the edge articulations into segments by row and column
segmentMap = new EdgeSegmentMap<E>(grid);
// compute row and column sizes
OrthogonalGridSizer<V, E> sizer =
new OrthogonalGridSizer<>(grid, segmentMap, transformer, edgeSpacing);
// get the grid coordinates from the sizer.
gridCoordinates = sizer.getGridCoordinates(isCondensed);
}
/**
* Returns a map of vertices to points in layout space.
* @return a map of vertices to points in layout space
*/
public Map<V, Point2D> getVertexLocations() {
Map<V, Point2D> vertexLocations = new HashMap<>();
for (Entry<V, GridPoint> entry : grid.getVertexPoints().entrySet()) {
V v = entry.getKey();
GridPoint p = entry.getValue();
Shape shape = transformer.apply(v);
Rectangle vertexBounds = shape.getBounds();
int x = gridCoordinates.x(p.col);
int y = gridCoordinates.y(p.row) + (vertexBounds.height >> 1);
Point2D location = new Point2D.Double(x, y);
vertexLocations.put(v, location);
}
return vertexLocations;
}
/**
* Returns a map of edges to lists of articulation points in layout space.
* @param vertexLocations the locations of vertices already mapped in layout space.
* @return a map of edges to lists of articulation points in layout space
*/
public Map<E, List<Point2D>> getEdgeLocations(Map<V, Point2D> vertexLocations) {
Map<E, List<Point2D>> edgeLocations = new HashMap<>();
for (E edge : grid.edges()) {
List<GridPoint> gridPoints = grid.getArticulations(edge);
List<Point2D> points = new ArrayList<>(gridPoints.size());
GridPoint gp = gridPoints.get(0);
EdgeSegment<E> edgeSegment = segmentMap.getColumnSegment(edge, gp);
int offset = edgeSegment.getOffset();
double x = gridCoordinates.x(gp.col) + offset * edgeSpacing;
double y = vertexLocations.get(edge.getStart()).getY();
points.add(new Point2D.Double(x, y));
for (int i = 1; i < gridPoints.size() - 1; i++) {
edgeSegment = edgeSegment.nextSegment();
gp = gridPoints.get(i);
if (i % 2 == 0) {
offset = edgeSegment.getOffset();
x = gridCoordinates.x(gp.col) + offset * edgeSpacing;
points.add(new Point2D.Double(x, y));
}
else {
offset = edgeSegment.getOffset();
y = gridCoordinates.y(gp.row) + offset * edgeSpacing;
points.add(new Point2D.Double(x, y));
}
}
gp = gridPoints.get(gridPoints.size() - 1);
y = vertexLocations.get(edge.getEnd()).getY();
points.add(new Point2D.Double(x, y));
edgeLocations.put(edge, points);
}
return edgeLocations;
}
/**
* Returns the computed grid coordinates.
* @return the computed grid coordinates
*/
public GridCoordinates getGridCoordinates() {
return gridCoordinates;
}
public List<Integer> getEdgeOffsets(E edge) {
List<Integer> offsets = new ArrayList<>();
List<GridPoint> gridPoints = grid.getArticulations(edge);
GridPoint gp = gridPoints.get(0);
EdgeSegment<E> edgeSegment = segmentMap.getColumnSegment(edge, gp);
while (edgeSegment != null) {
offsets.add(edgeSegment.getOffset());
edgeSegment = edgeSegment.nextSegment();
}
return offsets;
}
public void dispose() {
segmentMap.dispose();
grid.dispose();
gridCoordinates = null;
}
}

View file

@ -0,0 +1,267 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.List;
import ghidra.graph.viewer.layout.GridPoint;
/**
* Horizontal edge segments of an edge with articulation points. Each pair of points in the list
* of articulation points corresponds to either a column segment or a row segment. There is
* a built-in assumption in the compareTo algorithm that the list of articulation points always
* start and end with a column segment. See {@link EdgeSegment} for more information.
*
* @param <E> The edge type
*/
public class RowSegment<E> extends EdgeSegment<E> implements Comparable<RowSegment<E>> {
// specifies the orientation of the column attached to this segment (either left or right)
enum ColumnOrientation {
UP, // the attached column extends upwards from this row
DOWN // the attached column extends downwards from this row
}
private ColumnSegment<E> next;
private ColumnSegment<E> previous;
RowSegment(ColumnSegment<E> previous, E e, List<GridPoint> points, int pointIndex) {
super(e, points, pointIndex);
this.previous = previous;
// row segments always have a follow-on column segment
this.next = new ColumnSegment<E>(this, e, points, pointIndex + 1);
}
/**
* Returns the grid row index this row segment.
* @return the grid row index this row segment
*/
public int getRow() {
return points.get(pointIndex).row;
}
/**
* Return the index of the column where this row segment starts. Note that this is different
* from the left column. The start column is in the order of the articulation points whereas
* the left column is always the left most spatially column (lower column index) of either the
* start column or end column.
* @return the index of the grid column for the start point of this segment
*/
public int getStartCol() {
return points.get(pointIndex).col;
}
/**
* Return the index of the column where this row segment ends. Note that this is different
* from the right column. The end column is in the order of the articulation points whereas
* the right column is always the right most spatially column (higher column index) of either
* the start column or end column.
* @return the index of the grid column for the start point of this segment
*/
public int getEndCol() {
return points.get(pointIndex + 1).col;
}
/**
* Returns the column index of the left most column of this row segment.
* @return the column index of the left most column of this row segment
*/
public int getLeftCol() {
return Math.min(getStartCol(), getEndCol());
}
/**
* Returns the column index of the right most column of this row segment.
* @return the column index of the right most column of this row segment
*/
public int getRightCol() {
return Math.max(getStartCol(), getEndCol());
}
@Override
public int compareTo(RowSegment<E> other) {
int result = compareLefts(other);
if (result == 0) {
result = compareRights(other);
}
return result;
}
/**
* Checks if the given row segment overlaps horizontally.
* @param other the other column segment to compare
* @return true if these would overlap if drawn with same y row coordinate
*/
public boolean overlaps(RowSegment<E> other) {
if (getLeftCol() > other.getRightCol()) {
return false;
}
if (getRightCol() < other.getLeftCol()) {
return false;
}
// if the rows are exactly adjacent and if where they touch they are both attached
// to terminal column segments, then don't consider them to overlap.
if (getLeftCol() == other.getRightCol()) {
return !(isLeftTerminal() && other.isRightTerminal());
}
else if (getRightCol() == other.getLeftCol()) {
return !(isRightTerminal() && other.isLeftTerminal());
}
return true;
}
/**
* Compares row segments strictly based on the relationship of the connected column segments
* at the left of these segments.
* @param other the other row segment to compare
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*/
public int compareLefts(RowSegment<E> other) {
// first, just check the rows of this segment and the other, if the rows are
// not the same, then one column clearly above the other.
int result = getRow() - other.getRow();
if (result != 0) {
return result;
}
// We are in the same grid row, so the order is determined by the segment at the
// left of the column. There are 2 possible orientations for the left column. 1) it can
// extend upwards from our row, or it can extend downwards from our row.
// If our left orientation is not the same as the other row segment, the order is
// simply UP < DOWN in order to reduce edge crossings. Edges on the top
// go up and edges on the bottom go down, so they won't cross each other.
ColumnOrientation myLeftColOrientation = getOrientationForLeftColumn();
ColumnOrientation otherLeftColOrientation = other.getOrientationForLeftColumn();
// if they are not the same, then UP < DOWN
if (myLeftColOrientation != otherLeftColOrientation) {
return myLeftColOrientation.compareTo(otherLeftColOrientation);
}
// Both this segment's left and the other segment's left have the same orientation.
// Compare the left column segments and use those results to determine our ordering
// top to bottom. If the left columns extend upward, then which ever column is left most
// (lower value), should have it's associated row lower spatially (higher row value)
// Keeping left most columns(lower value) as lower rows allows their shape to avoid being
// crossed by the wider shape, which will be lower.
//
// And if the left rows extends downwards, the inverse applies.
ColumnSegment<E> myLeftColSegment = getLeftColSegment();
ColumnSegment<E> otherLeftColSegment = other.getLeftColSegment();
if (myLeftColOrientation == ColumnOrientation.UP) {
return -myLeftColSegment.compareTops(otherLeftColSegment);
}
return myLeftColSegment.compareBottoms(otherLeftColSegment);
}
/**
* Compares row segments strictly based on the relationship of the connected column segments
* at the right of these segments.
* @param other the other row segment to compare
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*/
public int compareRights(RowSegment<E> other) {
// first, just check the rows of this segment and the other, if the rows are
// not the same, then one column clearly above the other.
int result = getRow() - other.getRow();
if (result != 0) {
return result;
}
// We are in the same grid row, so the order is determined by the segment at the
// right of the column. There are 2 possible orientations for the right column. 1) it can
// extend upwards from our row, or it can extend downwards from our row.
// If our right orientation is not the same as the other row segment, the order is
// simply UP < DOWN in order to reduce edge crossings. Edges on the top
// go up and edges on the bottom go down, so they won't cross each other.
ColumnOrientation myRightColOrientation = getOrientationForRightColumn();
ColumnOrientation otherRightColOrientation = other.getOrientationForRightColumn();
// if they are not the same, then UP < DOWN
if (myRightColOrientation != otherRightColOrientation) {
return myRightColOrientation.compareTo(otherRightColOrientation);
}
// Both this segment's right column and the other segment's right column have the same
// orientation. Compare the right column segments and use those results to determine our
// ordering top to bottom. If the right columns extend upward, then which ever column is
// left most (lower value), should have it's associated row higher spatially (lower row
// value). Keeping left most columns(lower value) as higher rows (lower values) allows
// their shape to avoid being crossed by the wider shape, which will be lower.
//
// And if the right rows extends downwards, the inverse applies.
ColumnSegment<E> myRightColSegment = getRightColSegment();
ColumnSegment<E> otherRightColSegment = other.getRightColSegment();
if (myRightColOrientation == ColumnOrientation.UP) {
return myRightColSegment.compareTops(otherRightColSegment);
}
return -myRightColSegment.compareBottoms(otherRightColSegment);
}
@Override
public ColumnSegment<E> nextSegment() {
return next;
}
@Override
public ColumnSegment<E> previousSegment() {
return previous;
}
private ColumnOrientation getOrientationForLeftColumn() {
ColumnSegment<E> leftSegment = getLeftColSegment();
int leftColOtherRow = flowsLeft() ? leftSegment.getEndRow() : leftSegment.getStartRow();
return leftColOtherRow < getRow() ? ColumnOrientation.UP : ColumnOrientation.DOWN;
}
private ColumnOrientation getOrientationForRightColumn() {
ColumnSegment<E> rightSegment = getRightColSegment();
int rightColOtherRow = flowsLeft() ? rightSegment.getStartRow() : rightSegment.getEndRow();
return rightColOtherRow < getRow() ? ColumnOrientation.UP : ColumnOrientation.DOWN;
}
private ColumnSegment<E> getLeftColSegment() {
return flowsLeft() ? next : previous;
}
private ColumnSegment<E> getRightColSegment() {
return flowsLeft() ? previous : next;
}
private boolean flowsLeft() {
return getStartCol() > getEndCol();
}
private boolean isLeftTerminal() {
ColumnSegment<E> left = getLeftColSegment();
return left.isStartSegment() || left.isEndSegment();
}
private boolean isRightTerminal() {
ColumnSegment<E> right = getRightColSegment();
return right.isStartSegment() || right.isEndSegment();
}
public ColumnSegment<E> last() {
return next.last();
}
}

View file

@ -0,0 +1,116 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import java.util.*;
import org.apache.commons.collections4.map.LazyMap;
/**
* Maintains a collection of RowSegments for the same grid row.
*
* @param <E> edge type
*/
public class RowSegmentList<E> {
protected List<RowSegment<E>> edgeSegments = new ArrayList<>();
int row;
public RowSegmentList(int row) {
this.row = row;
}
protected void addSegment(RowSegment<E> segment) {
edgeSegments.add(segment);
}
@Override
public String toString() {
return edgeSegments.toString();
}
public int getRow() {
return row;
}
/**
* Returns the minimum offset of any edges in this segment list.
* @return the minimum offset of any edges in this segment list
*/
public int getMinOffset() {
int minOffset = 0;
for (EdgeSegment<E> edgeSegment : edgeSegments) {
minOffset = Math.min(minOffset, edgeSegment.getOffset());
}
return minOffset;
}
/**
* Returns the maximum offset of any edges in this segment list.
* @return the maximum offset of any edges in this segment list
*/
public int getMaxOffset() {
int maxOffset = 0;
for (EdgeSegment<E> edgeSegment : edgeSegments) {
maxOffset = Math.max(maxOffset, edgeSegment.getOffset());
}
return maxOffset;
}
/**
* Assigns offsets for overlapping row segments. Parallel overlapping edges must be offset
* from each other when assigned to layout space to avoid drawing over each other. Each
* edge offset represents 1/2 the edge spacing distance. The reason offsets are assigned 2
* apart from each other is to be consistent with column segment offsets which must be 2 apart
* so that even numbers of edges can be centered on the grid line.
*/
public void assignOffsets() {
// sorts the row edge segments from top to bottom
Collections.sort(edgeSegments);
Map<Integer, RowSegmentList<E>> offsetMap =
LazyMap.lazyMap(new HashMap<>(), k -> new RowSegmentList<E>(0));
for (int i = 0; i < edgeSegments.size(); i++) {
RowSegment<E> segment = edgeSegments.get(i);
assignOffset(offsetMap, segment);
}
}
protected void assignOffset(Map<Integer, RowSegmentList<E>> offsetMap, RowSegment<E> segment) {
// assigning offsets to rows is easy, just find the first offset group that we don't
// overlap with.
int offset = 0;
RowSegmentList<E> segments = offsetMap.get(offset);
while (segments.hasOverlappingSegment(segment)) {
offset += 2;
segments = offsetMap.get(offset);
}
segment.setOffset(offset);
segments.addSegment(segment);
}
private boolean hasOverlappingSegment(RowSegment<E> segment) {
for (RowSegment<E> edgeSegment : edgeSegments) {
if (segment.overlaps(edgeSegment)) {
return true;
}
}
return false;
}
}

View file

@ -56,7 +56,7 @@ public class JgtNamedLayout extends AbstractFGLayout {
}
@Override
protected Point2D getVertexLocation(FGVertex v, Column col, Row<FGVertex> row,
protected Point2D getVertexLocation(FGVertex v, Column<FGVertex> col, Row<FGVertex> row,
java.awt.Rectangle bounds) {
return getCenteredVertexLocation(v, col, row, bounds);
}
@ -148,14 +148,14 @@ public class JgtNamedLayout extends AbstractFGLayout {
for (FGEdge fgEdge : edges) {
monitor.checkCancelled();
List<java.awt.Point> newPoints = new ArrayList<>();
List<GridPoint> newPoints = new ArrayList<>();
List<Point> articulations = articulator.apply(fgEdge);
for (Point point : articulations) {
Integer col = columns.get(point.x);
Integer row = rows.get(point.y);
newPoints.add(new java.awt.Point(col, row));
newPoints.add(new GridPoint(row, col));
}
// The jung layout will provide articulations at the vertex points. We do not want to

View file

@ -627,10 +627,13 @@ public class FGViewUpdater extends VisualGraphViewUpdater<FGVertex, FGEdge> {
parentVertex.setVertexType(oldVertexType);
childVertex.setVertexType(oldVertexType);
}
graph.addVertex(parentVertex);
graph.addVertex(childVertex);
FunctionGraph fg = (FunctionGraph) graph;
if (vertexToSplit == fg.getRootVertex()) {
fg.setRootVertex(parentVertex);
}
//
// Second: add the edges from the original vertices to the new vertices
//

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

View file

@ -77,7 +77,7 @@ public class TestFGLayoutProvider extends FGLayoutProvider {
}
@Override
protected Point2D getVertexLocation(FGVertex v, Column col, Row<FGVertex> row,
protected Point2D getVertexLocation(FGVertex v, Column<FGVertex> col, Row<FGVertex> row,
Rectangle bounds) {
return getCenteredVertexLocation(v, col, row, bounds);
}
@ -257,8 +257,8 @@ public class TestFGLayoutProvider extends FGLayoutProvider {
// -->Maybe positioning is simple enough?
//
Column startCol = layoutLocations.col(startVertex);
Column endCol = layoutLocations.col(endVertex);
Column<FGVertex> startCol = layoutLocations.col(startVertex);
Column<FGVertex> endCol = layoutLocations.col(endVertex);
Point2D start = vertexLayoutLocations.get(startVertex);
Point2D end = vertexLayoutLocations.get(endVertex);
List<Point2D> articulations = new ArrayList<>();

View file

@ -0,0 +1,420 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import static org.junit.Assert.*;
import java.awt.*;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Function;
import javax.swing.JFrame;
import org.junit.Before;
import docking.test.AbstractDockingTest;
import ghidra.graph.VisualGraph;
import ghidra.graph.graphs.*;
import ghidra.graph.support.TestVisualGraph;
import ghidra.graph.viewer.GraphComponent;
import ghidra.graph.viewer.layout.*;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
abstract public class AbstractFlowChartLayoutTest extends AbstractDockingTest {
protected TestVisualGraph g;
protected GridLocationMap<AbstractTestVertex, TestEdge> grid;
protected OrthogonalGridToLayoutMapper<AbstractTestVertex, TestEdge> layoutMap;
private boolean alignLeft;
protected LabelTestVertex A = v('A');
protected LabelTestVertex B = v('B');
protected LabelTestVertex C = v('C');
protected LabelTestVertex D = v('D');
protected LabelTestVertex E = v('E');
protected LabelTestVertex F = v('F');
protected LabelTestVertex G = v('G');
protected LabelTestVertex H = v('H');
protected LabelTestVertex I = v('I');
protected LabelTestVertex J = v('J');
protected LabelTestVertex K = v('K');
protected AbstractFlowChartLayoutTest(boolean alignLeft) {
this.alignLeft = alignLeft;
}
@Before
public void setUp() {
g = new TestVisualGraph();
}
protected void showGraph() {
Swing.runNow(() -> {
JFrame frame = new JFrame("Graph Viewer Test");
TestFlowChartLayout layout = new TestFlowChartLayout(g, A, alignLeft);
g.setLayout(layout);
GraphComponent<AbstractTestVertex, TestEdge, TestVisualGraph> graphComponent =
new GraphComponent<>(g);
graphComponent.setSatelliteVisible(false);
frame.setSize(new Dimension(800, 800));
frame.setLocation(2400, 100);
frame.getContentPane().add(graphComponent.getComponent());
frame.setVisible(true);
frame.validate();
});
}
protected void assertEdge(EdgeVerifier edgeVerifier) {
edgeVerifier.checkEdge();
}
protected void assertVertices(String expected) {
String actual = generateCompactGridVertexString();
if (!expected.equals(actual)) {
reportGridError(expected, actual);
}
}
private void reportGridError(String expectedString, String actualString) {
String[] actual = StringUtilities.toLines(actualString, false);
String[] expected = StringUtilities.toLines(expectedString, false);
assertEquals("Number of rows don't match! ", expected.length, actual.length);
checkCols(expected);
assertEquals("Number of columns don't match! ", expected[0].length(), actual[0].length());
if (!expectedString.equals(actualString)) {
printGridsToConsole(actual, expected);
int firstRowDiff = findFirstRowDiff(actual, expected);
int firstColDiff = findFirstColDiff(actual[firstRowDiff], expected[firstRowDiff]);
fail("Graphs don't match! First diff is at row " + firstRowDiff + ", col " +
firstColDiff + ". See console for details.");
}
}
private int findFirstColDiff(String string1, String string2) {
for (int i = 0; i < string1.length(); i++) {
if (string1.charAt(i) != string2.charAt(i)) {
return i;
}
}
return -1;
}
private int findFirstRowDiff(String[] actual, String[] expected) {
for (int i = 0; i < actual.length; i++) {
if (!actual[i].equals(expected[i])) {
return i;
}
}
return -1;
}
private void printGridsToConsole(String[] actual, String[] expected) {
StringWriter writer = new StringWriter();
PrintWriter out = new PrintWriter(writer);
out.println("Graphs don't match!\n");
String expectedLabel = "Expected: ";
String actualLabel = " Actual: ";
String diffLabel = " Diffs: ";
String[] diffs = computeDiffs(actual, expected);
for (int row = 0; row < expected.length; row++) {
out.print(expectedLabel);
out.print(expected[row]);
out.print(actualLabel);
out.print(actual[row]);
out.print(diffLabel);
out.println(diffs[row]);
expectedLabel = " ";
actualLabel = expectedLabel;
diffLabel = expectedLabel;
}
Msg.error(this, writer.toString());
}
private String[] computeDiffs(String[] actual, String[] expected) {
String[] diffs = new String[actual.length];
for (int i = 0; i < diffs.length; i++) {
diffs[i] = computeDiffString(actual[i], expected[i]);
}
return diffs;
}
private String computeDiffString(String string1, String string2) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < string1.length(); i++) {
char c = string1.charAt(i) == string2.charAt(i) ? '.' : 'x';
buf.append(c);
}
return buf.toString();
}
private void checkCols(String[] expected) {
int len = expected[0].length();
for (String line : expected) {
if (line.length() != len) {
fail("Invalid graph grid specified in test. Lines vary in length!");
}
}
}
private String generateCompactGridEdgeString(TestEdge e, List<GridPoint> points) {
char[][] gridChars = createEmptyGridChars();
fillGridCharColumn(gridChars, points.get(0), points.get(1));
for (int i = 1; i < points.size() - 2; i++) {
fillGridCharRow(gridChars, points.get(i), points.get(i + 1));
fillGridCharColumn(gridChars, points.get(i + 1), points.get(i + 2));
}
AbstractTestVertex start = e.getStart();
AbstractTestVertex end = e.getEnd();
GridPoint startPoint = grid.gridPoint(start);
GridPoint endPoint = grid.gridPoint(end);
gridChars[startPoint.row][startPoint.col] = start.toString().charAt(0);
gridChars[endPoint.row][endPoint.col] = end.toString().charAt(0);
return toString(gridChars);
}
private void fillGridCharRow(char[][] gridChars, GridPoint p1, GridPoint p2) {
int row = p1.row;
int startCol = Math.min(p1.col, p2.col);
int endCol = Math.max(p1.col, p2.col);
for (int col = startCol; col <= endCol; col++) {
gridChars[row][col] = '*';
}
}
private void fillGridCharColumn(char[][] gridChars, GridPoint p1, GridPoint p2) {
int col = p1.col;
int startRow = Math.min(p1.row, p2.row);
int endRow = Math.max(p1.row, p2.row);
for (int row = startRow; row <= endRow; row++) {
gridChars[row][col] = '*';
}
}
private String generateCompactGridVertexString() {
char[][] gridChars = createEmptyGridChars();
for (Entry<AbstractTestVertex, GridPoint> entry : grid.getVertexPoints().entrySet()) {
char id = entry.getKey().toString().charAt(0);
GridPoint point = entry.getValue();
gridChars[point.row][point.col] = id;
}
return toString(gridChars);
}
private String toString(char[][] gridChars) {
StringBuilder buf = new StringBuilder();
for (int row = 0; row < grid.height(); row++) {
for (int col = 0; col < grid.width(); col++) {
buf.append(gridChars[row][col]);
}
buf.append("\n");
}
return buf.toString();
}
private char[][] createEmptyGridChars() {
int rows = grid.height();
int cols = grid.width();
char[][] gridChars = new char[rows][cols];
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
gridChars[row][col] = '.';
}
}
return gridChars;
}
protected EdgeVerifier e(LabelTestVertex start, LabelTestVertex end) {
return new EdgeVerifier(start, end);
}
protected class EdgeVerifier {
private TestEdge edge;
private List<GridPoint> expectedPoints = new ArrayList<>();
private List<Integer> expectedOffsets = new ArrayList<>();
public EdgeVerifier(LabelTestVertex start, LabelTestVertex end) {
this.edge = new TestEdge(start, end);
expectedPoints.add(grid.gridPoint(start));
}
public EdgeVerifier colSegment(int rows, int offset) {
GridPoint lastPoint = expectedPoints.get(expectedPoints.size() - 1);
GridPoint p = new GridPoint(lastPoint.row + rows, lastPoint.col);
if (!grid.containsPoint(p)) {
fail("Bad column specification. Row movement of " + rows + " exceeds grid size!");
}
expectedPoints.add(p);
expectedOffsets.add(offset);
return this;
}
public EdgeVerifier rowSegment(int cols, int offset) {
GridPoint lastPoint = expectedPoints.get(expectedPoints.size() - 1);
GridPoint p = new GridPoint(lastPoint.row, lastPoint.col + cols);
if (!grid.containsPoint(p)) {
fail("Bad row specification. Column movement of " + cols + " exceeds grid size!");
}
expectedPoints.add(p);
expectedOffsets.add(offset);
return this;
}
public void checkEdge() {
List<GridPoint> actual = grid.getArticulations(edge);
if (expectedPoints.size() < 2) {
fail("Specified Edge for edge " + edge + " is missing segment specifications!");
}
if (actual.isEmpty()) {
fail("Expected edge articulations for " + edge + "not found!");
}
if (!actual.equals(expectedPoints)) {
printEdgeDiffs(actual);
fail("Articulations for edge " + edge + " don't match! See console for details");
}
List<Integer> actualOffsets = layoutMap.getEdgeOffsets(edge);
for (int i = 0; i < expectedOffsets.size(); i++) {
GridPoint p1 = expectedPoints.get(i);
GridPoint p2 = expectedPoints.get(i + 1);
assertEquals(
"Edge Offsets differ for edge " + edge + " at segment " + i +
", from " + p1 + " to " + p2 + "! ",
expectedOffsets.get(i), actualOffsets.get(i));
}
}
private void printEdgeDiffs(List<GridPoint> actualPoints) {
StringWriter writer = new StringWriter();
PrintWriter out = new PrintWriter(writer);
String expectedString = generateCompactGridEdgeString(edge, expectedPoints);
String actualString = generateCompactGridEdgeString(edge, actualPoints);
String[] expected = StringUtilities.toLines(expectedString, false);
String[] actual = StringUtilities.toLines(actualString, false);
out.println("Edge articulations don't match for " + edge.toString() + "!\n");
String expectedLabel = "Expected: ";
String actualLabel = " Actual: ";
for (int row = 0; row < expected.length; row++) {
out.print(expectedLabel);
out.print(expected[row]);
out.print(actualLabel);
out.println(actual[row]);
expectedLabel = " ";
actualLabel = expectedLabel;
}
Msg.error(this, writer.toString());
}
}
protected int offset(int i) {
return i;
}
protected int down(int i) {
return i;
}
protected int up(int i) {
return -i;
}
protected int left(int i) {
return -i;
}
protected int right(int i) {
return i;
}
protected void applyLayout() throws CancelledException {
TestFlowChartLayout layout = new TestFlowChartLayout(g, A, alignLeft);
grid = layout.performInitialGridLayout(g);
Function<AbstractTestVertex, Shape> transformer = v -> new Rectangle(0, 0, 50, 50);
layoutMap =
new OrthogonalGridToLayoutMapper<AbstractTestVertex, TestEdge>(grid, transformer, true);
}
// a shortcut for edge(v(startId), v(endId)), for readability
protected TestEdge edge(LabelTestVertex v1, LabelTestVertex v2) {
TestEdge testEdge = new TestEdge(v1, v2);
g.addEdge(testEdge);
return testEdge;
}
protected LabelTestVertex v(char id) {
return new LabelTestVertex("" + id);
}
private static class TestFlowChartLayout
extends AbstractFlowChartLayout<AbstractTestVertex, TestEdge> {
private AbstractTestVertex root;
protected TestFlowChartLayout(DefaultVisualGraph<AbstractTestVertex, TestEdge> graph,
AbstractTestVertex root, boolean leftAlign) {
super(graph, new TestEdgeComparator(), leftAlign);
this.root = root;
}
@Override
public AbstractVisualGraphLayout<AbstractTestVertex, TestEdge> createClonedLayout(
VisualGraph<AbstractTestVertex, TestEdge> newGraph) {
throw new UnsupportedOperationException();
}
@Override
protected AbstractTestVertex getRoot(VisualGraph<AbstractTestVertex, TestEdge> g) {
return root;
}
@Override
protected GridLocationMap<AbstractTestVertex, TestEdge> performInitialGridLayout(
VisualGraph<AbstractTestVertex, TestEdge> g) throws CancelledException {
return super.performInitialGridLayout(g);
}
}
private static class TestEdgeComparator implements Comparator<TestEdge> {
@Override
public int compare(TestEdge e1, TestEdge e2) {
return e1.toString().compareTo(e2.toString());
}
}
}

View file

@ -0,0 +1,264 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import org.junit.Before;
import org.junit.Test;
import ghidra.graph.support.TestVisualGraph;
import ghidra.util.exception.CancelledException;
public class FlowChartLayoutTest extends AbstractFlowChartLayoutTest {
public FlowChartLayoutTest() {
super(false);
}
@Override
@Before
public void setUp() {
g = new TestVisualGraph();
}
@Test
public void testBasicRootWithTwoChildren() throws CancelledException {
edge(A, B);
edge(A, C);
applyLayout();
showGraph();
assertVertices("""
....
..A.
....
.B.C
""");
assertEdge(e(A, B)
.colSegment(down(1), offset(-1))
.rowSegment(left(1), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(A, C)
.colSegment(down(1), offset(1))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(0)));
}
@Test
public void testBasicGraphWithThreeChildren() throws CancelledException {
edge(A, B);
edge(A, C);
edge(A, D);
applyLayout();
// showGraph();
assertVertices("""
......
...A..
......
.B.C.D
""");
assertEdge(e(A, B)
.colSegment(down(1), offset(-2))
.rowSegment(left(2), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(A, C)
.colSegment(down(2), offset(0)));
assertEdge(e(A, D)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(0))
.colSegment(down(1), offset(0)));
}
@Test
public void testBasicGraphWithBackEdge() throws CancelledException {
edge(A, B);
edge(A, C);
edge(A, D);
edge(D, A);
applyLayout();
// showGraph();
assertVertices("""
.......
...A...
.......
.B.C.D.
.......
""");
assertEdge(e(A, B)
.colSegment(down(1), offset(-2))
.rowSegment(left(2), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(A, C)
.colSegment(down(2), offset(0)));
assertEdge(e(A, D)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(D, A)
.colSegment(down(1), offset(0))
.rowSegment(right(1), offset(0))
.colSegment(up(4), offset(0))
.rowSegment(left(3), offset(0))
.colSegment(down(1), offset(0)));
}
@Test
public void testComplexGraph() throws CancelledException {
//@formatter:off
/*
A
|
B
|
sink C <---D
| |
| E
| // \
| |F G
| ||\ |
| |H | /
| | \|/
| | I
| | / \
| |/ \
.<-K---->J sink
*/
//@formatter:on edge(A, B);
edge(A, B);
edge(B, D);
edge(D, C);
edge(D, E);
edge(E, F);
edge(E, G);
edge(E, K);
edge(F, H);
edge(F, I);
edge(G, I);
edge(H, I);
edge(I, J);
edge(I, K);
edge(K, C);
edge(K, J);
applyLayout();
// showGraph();
assertVertices("""
.....
...A.
.....
...B.
.....
...D.
.....
...E.
.....
..F.G
.....
..H..
.....
..I..
.....
..K..
.....
.C.J.
""");
assertEdge(e(A, B)
.colSegment(down(2), offset(0)));
assertEdge(e(B, D)
.colSegment(down(2), offset(0)));
assertEdge(e(D, C)
.colSegment(down(1), offset(-2))
.rowSegment(left(2), offset(0))
.colSegment(down(11), offset(-1)));
assertEdge(e(D, E)
.colSegment(down(2), offset(0)));
assertEdge(e(E, F)
.colSegment(down(1), offset(-1))
.rowSegment(left(1), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(E, G)
.colSegment(down(1), offset(3))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(E, K)
.colSegment(down(7), offset(1))
.rowSegment(left(1), offset(2))
.colSegment(down(1), offset(2)));
assertEdge(e(F, H)
.colSegment(down(2), offset(0)));
assertEdge(e(F, I)
.colSegment(down(1), offset(-2))
.rowSegment(left(1), offset(0))
.colSegment(down(2), offset(1))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(-2)));
assertEdge(e(G, I)
.colSegment(down(3), offset(0))
.rowSegment(left(2), offset(0))
.colSegment(down(1), offset(2)));
assertEdge(e(H, I)
.colSegment(down(2), offset(0)));
assertEdge(e(I, J)
.colSegment(down(1), offset(2))
.rowSegment(right(1), offset(0))
.colSegment(down(3), offset(-1)));
assertEdge(e(I, K)
.colSegment(down(2), offset(0)));
assertEdge(e(K, C)
.colSegment(down(1), offset(-1))
.rowSegment(left(1), offset(0))
.colSegment(down(1), offset(1)));
assertEdge(e(K, J)
.colSegment(down(1), offset(1))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(-3)));
}
}

View file

@ -0,0 +1,258 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import org.junit.Test;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
public class LeftAlignedFlowChartLayoutTest extends AbstractFlowChartLayoutTest {
public LeftAlignedFlowChartLayoutTest() {
super(true);
}
@Test
public void testBasicRootWithTwoChildren() throws CancelledException {
edge(A, B);
edge(A, C);
applyLayout();
// showGraph();
// Msg.out(grid.toStringGrid());
assertVertices("""
....
.A..
....
.B.C
""");
assertEdge(e(A, B)
.colSegment(down(2), offset(0)));
assertEdge(e(A, C)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(0))
.colSegment(down(1), offset(0)));
}
@Test
public void testBasicGraphWithThreeChildren() throws CancelledException {
edge(A, B);
edge(A, C);
edge(A, D);
applyLayout();
// showGraph();
Msg.out(grid.toStringGrid());
assertVertices("""
......
.A....
......
.B.C.D
""");
assertEdge(e(A, B)
.colSegment(down(2), offset(0)));
assertEdge(e(A, C)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(2))
.colSegment(down(1), offset(0)));
assertEdge(e(A, D)
.colSegment(down(1), offset(4))
.rowSegment(right(4), offset(0))
.colSegment(down(1), offset(0)));
}
@Test
public void testBasicGraphWithBackEdge() throws CancelledException {
edge(A, B);
edge(A, C);
edge(A, D);
edge(D, A);
applyLayout();
// showGraph();
// Msg.out(grid.toStringGrid());
assertVertices("""
.......
.A.....
.......
.B.C.D.
.......
""");
assertEdge(e(A, B)
.colSegment(down(2), offset(0)));
assertEdge(e(A, C)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(2))
.colSegment(down(1), offset(0)));
assertEdge(e(A, D)
.colSegment(down(1), offset(4))
.rowSegment(right(4), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(D, A)
.colSegment(down(1), offset(0))
.rowSegment(right(1), offset(0))
.colSegment(up(4), offset(0))
.rowSegment(left(5), offset(0))
.colSegment(down(1), offset(0)));
}
@Test
public void testComplexGraph() throws CancelledException {
//@formatter:off
/*
A
|
B
|
sink C <---D
| |
| E
| // \
| |F G
| ||\ |
| |H | /
| | \|/
| | I
| | / \
| |/ \
.<-K---->J sink
*/
//@formatter:on edge(A, B);
edge(A, B);
edge(B, D);
edge(D, C);
edge(D, E);
edge(E, F);
edge(E, G);
edge(E, K);
edge(F, H);
edge(F, I);
edge(G, I);
edge(H, I);
edge(I, J);
edge(I, K);
edge(K, C);
edge(K, J);
applyLayout();
showGraph();
Msg.out(grid.toStringGrid());
assertVertices("""
....
.A..
....
.B..
....
.D..
....
.E..
....
.F.G
....
.H..
....
.I..
....
.K..
....
.C.J
""");
assertEdge(e(A, B)
.colSegment(down(2), offset(0)));
assertEdge(e(B, D)
.colSegment(down(2), offset(0)));
assertEdge(e(D, C)
.colSegment(down(1), offset(-2))
.rowSegment(left(1), offset(0))
.colSegment(down(10), offset(-2))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(-2)));
assertEdge(e(D, E)
.colSegment(down(2), offset(0)));
assertEdge(e(E, F)
.colSegment(down(2), offset(0)));
assertEdge(e(E, G)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(0))
.colSegment(down(1), offset(0)));
assertEdge(e(E, K)
.colSegment(down(1), offset(-2))
.rowSegment(left(1), offset(0))
.colSegment(down(6), offset(0))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(-2)));
assertEdge(e(F, H)
.colSegment(down(2), offset(0)));
assertEdge(e(F, I)
.colSegment(down(1), offset(-2))
.rowSegment(left(1), offset(0))
.colSegment(down(2), offset(2))
.rowSegment(right(1), offset(0))
.colSegment(down(1), offset(-2)));
assertEdge(e(G, I)
.colSegment(down(3), offset(0))
.rowSegment(left(2), offset(0))
.colSegment(down(1), offset(2)));
assertEdge(e(H, I)
.colSegment(down(2), offset(0)));
assertEdge(e(I, J)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(0))
.colSegment(down(3), offset(1)));
assertEdge(e(I, K)
.colSegment(down(2), offset(0)));
assertEdge(e(K, C)
.colSegment(down(2), offset(0)));
assertEdge(e(K, J)
.colSegment(down(1), offset(2))
.rowSegment(right(2), offset(0))
.colSegment(down(1), offset(-1)));
}
}

View file

@ -0,0 +1,982 @@
/* ###
* 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.app.plugin.core.functiongraph.graph.layout.flowchart;
import static org.junit.Assert.*;
import java.util.*;
import org.junit.Test;
import ghidra.graph.graphs.TestEdge;
import ghidra.graph.graphs.TestVertex;
import ghidra.graph.viewer.layout.GridPoint;
public class EdgeSegmentTest {
private static int DOWN = 1;
private static int UP = -1;
private static int LEFT = -1;
private static int RIGHT = 1;
private static int UP_2 = -2;
private static int DOWN_2 = 2;
private static int LEFT_2 = -2;
private static int RIGHT_2 = 2;
private TestVertex v1 = v(1);
private TestVertex v2 = v(2);
private TestVertex v3 = v(3);
private TestVertex v4 = v(4);
private TestEdge e12 = e(v1, v2);
private TestEdge e13 = e(v1, v2);
private TestEdge e14 = e(v1, v4);
private TestEdge e23 = e(v2, v3);
private TestEdge e24 = e(v2, v4);
private TestEdge e34 = e(v3, v4);
//==================================================================================================
// The following tests compare starting edge segments.
//=================================================================================================
@Test
public void testStartSegmentsInTotallyDifferentColumns() {
ColumnSegment<TestEdge> col1 = segment(e12, p(0, 1), DOWN);
ColumnSegment<TestEdge> col2 = segment(e13, p(0, 2), DOWN);
assertLessThan(col1, col2);
assertGreaterThan(col2, col1);
}
@Test
public void testCompareStartSegmentByDirectionOfBottomRow() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> down = segment(e12, p, DOWN_2);
ColumnSegment<TestEdge> left = segment(e13, p, DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> right = segment(e14, p, DOWN, RIGHT, DOWN);
assertLessThan(left, down);
assertGreaterThan(down, left);
assertLessThan(down, right);
assertGreaterThan(right, down);
assertLessThan(left, right);
assertGreaterThan(right, left);
// test edges are equals to themselves
assertEquals(down, down);
assertEquals(left, left);
assertEquals(right, right);
}
@Test
public void testCompareStartBothLeftButDifferentRowLevels() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> down1_left = segment(e13, p, DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> down2_left = segment(e14, p, DOWN_2, LEFT, DOWN);
assertLessThan(down1_left, down2_left);
assertGreaterThan(down2_left, down1_left);
}
@Test
public void testCompareStartBothRightButDifferentRowLevels() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> down1_right = segment(e13, p, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> down2_right = segment(e14, p, DOWN_2, RIGHT, DOWN);
assertLessThan(down2_right, down1_right);
assertGreaterThan(down1_right, down2_right);
}
@Test
public void testCompareStartBothLeftThenOppositeDirection() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> leftUp = segment(e13, p, DOWN, LEFT, UP);
ColumnSegment<TestEdge> leftDown = segment(e14, p, DOWN, LEFT, DOWN);
assertLessThan(leftUp, leftDown);
assertGreaterThan(leftDown, leftUp);
}
@Test
public void testCompareStartBothRightsThenOppositeDirection() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> rightUp = segment(e13, p, DOWN, RIGHT, UP);
ColumnSegment<TestEdge> rightDown = segment(e14, p, DOWN, RIGHT, DOWN);
assertLessThan(rightDown, rightUp);
assertGreaterThan(rightUp, rightDown);
}
@Test
public void testCompareLeftDownButDifferentLeftColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> left1 = segment(e13, p, DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> left2 = segment(e14, p, DOWN, LEFT_2, DOWN);
assertLessThan(left2, left1);
assertGreaterThan(left1, left2);
}
@Test
public void testCompareLeftUpButDifferentLeftColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> left1 = segment(e13, p, DOWN, LEFT, UP);
ColumnSegment<TestEdge> left2 = segment(e14, p, DOWN, LEFT_2, UP);
assertLessThan(left1, left2);
assertGreaterThan(left2, left1);
}
@Test
public void testCompareRightDownButDifferentRightColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> right1 = segment(e13, p, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> right2 = segment(e14, p, DOWN, RIGHT_2, DOWN);
assertLessThan(right1, right2);
assertGreaterThan(right2, right1);
}
@Test
public void testCompareRightUpButDifferentRightColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> right1 = segment(e13, p, DOWN, RIGHT, UP);
ColumnSegment<TestEdge> right2 = segment(e14, p, DOWN, RIGHT_2, UP);
assertLessThan(right2, right1);
assertGreaterThan(right1, right2);
}
@Test
public void testCompareLeftUpButUpperRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_u_left = segment(e13, p, DOWN, LEFT, UP, LEFT);
ColumnSegment<TestEdge> l_u_right = segment(e14, p, DOWN, LEFT, UP, RIGHT);
assertLessThan(l_u_right, l_u_left);
assertGreaterThan(l_u_left, l_u_right);
}
@Test
public void testCompareLeftDownButLowerRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_d_left = segment(e13, p, DOWN, LEFT, DOWN, LEFT);
ColumnSegment<TestEdge> l_d_right = segment(e14, p, DOWN, LEFT, DOWN, RIGHT);
assertLessThan(l_d_left, l_d_right);
assertGreaterThan(l_d_right, l_d_left);
}
@Test
public void testCompareRightUpButUpperRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_u_left = segment(e13, p, DOWN, RIGHT, UP, LEFT);
ColumnSegment<TestEdge> r_u_right = segment(e14, p, DOWN, RIGHT, UP, RIGHT);
assertLessThan(r_u_right, r_u_left);
assertGreaterThan(r_u_left, r_u_right);
}
@Test
public void testCompareRightDownButLowerRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_d_left = segment(e13, p, DOWN, RIGHT, DOWN, LEFT);
ColumnSegment<TestEdge> r_d_right = segment(e14, p, DOWN, RIGHT, DOWN, RIGHT);
assertLessThan(r_d_left, r_d_right);
assertGreaterThan(r_d_right, r_d_left);
}
@Test
public void testCompareLeftUpLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_u_left = segment(e13, p, DOWN, LEFT, UP, LEFT, DOWN);
ColumnSegment<TestEdge> l_u_left2 = segment(e14, p, DOWN, LEFT, UP, LEFT_2, DOWN);
assertLessThan(l_u_left2, l_u_left);
assertGreaterThan(l_u_left, l_u_left2);
}
@Test
public void testCompareLeftUpRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_u_right = segment(e13, p, DOWN, LEFT, UP, RIGHT, DOWN);
ColumnSegment<TestEdge> l_u_right2 = segment(e14, p, DOWN, LEFT, UP, RIGHT_2, DOWN);
assertLessThan(l_u_right, l_u_right2);
assertGreaterThan(l_u_right2, l_u_right);
}
@Test
public void testCompareLeftDownLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_d_left = segment(e13, p, DOWN, LEFT, DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> l_d_left2 = segment(e14, p, DOWN, LEFT, DOWN, LEFT_2, DOWN);
assertLessThan(l_d_left2, l_d_left);
assertGreaterThan(l_d_left, l_d_left2);
}
@Test
public void testCompareLeftDownRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_d_right = segment(e13, p, DOWN, LEFT, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> l_d_right2 = segment(e14, p, DOWN, LEFT, DOWN, RIGHT_2, DOWN);
assertLessThan(l_d_right, l_d_right2);
assertGreaterThan(l_d_right2, l_d_right);
}
@Test
public void testCompareRightUpLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_u_left = segment(e13, p, DOWN, RIGHT, UP, LEFT, DOWN);
ColumnSegment<TestEdge> r_u_left2 = segment(e14, p, DOWN, RIGHT, UP, LEFT_2, DOWN);
assertLessThan(r_u_left2, r_u_left);
assertGreaterThan(r_u_left, r_u_left2);
}
@Test
public void testCompareRightUpRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_u_right = segment(e13, p, DOWN, RIGHT, UP, RIGHT, DOWN);
ColumnSegment<TestEdge> r_u_right2 = segment(e14, p, DOWN, RIGHT, UP, RIGHT_2, DOWN);
assertLessThan(r_u_right, r_u_right2);
assertGreaterThan(r_u_right2, r_u_right);
}
@Test
public void testCompareRightDownLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_d_left = segment(e13, p, DOWN, RIGHT, DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> r_d_left2 = segment(e14, p, DOWN, RIGHT, DOWN, LEFT_2, DOWN);
assertLessThan(r_d_left2, r_d_left);
assertGreaterThan(r_d_left, r_d_left2);
}
@Test
public void testCompareRightDownRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_d_right = segment(e13, p, DOWN, RIGHT, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> r_d_right2 = segment(e14, p, DOWN, RIGHT, DOWN, RIGHT_2, DOWN);
assertLessThan(r_d_right, r_d_right2);
assertGreaterThan(r_d_right2, r_d_right);
}
//==================================================================================================
// The following tests compare ending edge segments.
//=================================================================================================
@Test
public void testCompareEndsInTotallyDifferentColumns() {
GridPoint p1 = p(0, 0);
GridPoint p2 = p(0, 1);
ColumnSegment<TestEdge> col0 = endSegment(e12, p1, UP_2);
ColumnSegment<TestEdge> col1 = endSegment(e12, p2, UP_2);
assertLessThan(col0, col1);
assertGreaterThan(col1, col0);
}
@Test
public void testCompareEndsFromDifferentDirections() {
GridPoint p = p(5, 5);
ColumnSegment<TestEdge> fromRight = endSegment(e14, p, UP, RIGHT, UP);
ColumnSegment<TestEdge> fromAbove = endSegment(e24, p, UP);
ColumnSegment<TestEdge> fromLeft = endSegment(e34, p, UP, LEFT, UP);
assertLessThan(fromLeft, fromAbove);
assertLessThan(fromAbove, fromRight);
assertLessThan(fromLeft, fromRight);
assertGreaterThan(fromAbove, fromLeft);
assertGreaterThan(fromRight, fromAbove);
assertGreaterThan(fromRight, fromAbove);
}
@Test
public void testCompareEndBothLeftButDifferentRowLevels() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> up1_left = endSegment(e13, p, UP, LEFT, UP);
ColumnSegment<TestEdge> up2_left = segment(e23, p, UP_2, LEFT, UP);
assertLessThan(up1_left, up2_left);
assertGreaterThan(up2_left, up1_left);
}
@Test
public void testCompareEndBothRightButDifferentRowLevels() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> up1_right = endSegment(e13, p, UP, RIGHT, UP);
ColumnSegment<TestEdge> up2_right = segment(e23, p, UP_2, RIGHT, UP);
assertLessThan(up2_right, up1_right);
assertGreaterThan(up1_right, up2_right);
}
@Test
public void testCompareEndBothLeftThenOppositeDirection() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> u_l_up = endSegment(e13, p, UP, LEFT, UP);
ColumnSegment<TestEdge> u_l_down = endSegment(e14, p, UP, LEFT, DOWN);
assertLessThan(u_l_down, u_l_up);
assertGreaterThan(u_l_up, u_l_down);
}
@Test
public void testCompareEndBothRightsThenOppositeDirection() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> u_r_up = endSegment(e13, p, UP, RIGHT, UP);
ColumnSegment<TestEdge> u_r_down = endSegment(e14, p, UP, RIGHT, DOWN);
assertLessThan(u_r_up, u_r_down);
assertGreaterThan(u_r_down, u_r_up);
}
@Test
public void testCompareEndsLeftDownButDifferentLeftColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> left1 = endSegment(e13, p, UP, LEFT, DOWN);
ColumnSegment<TestEdge> left2 = endSegment(e14, p, UP, LEFT_2, DOWN);
assertLessThan(left1, left2);
assertGreaterThan(left2, left1);
}
@Test
public void testCompareEndsLeftUpButDifferentLeftColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> left1 = endSegment(e13, p, UP, LEFT, UP);
ColumnSegment<TestEdge> left2 = endSegment(e14, p, UP, LEFT_2, UP);
assertLessThan(left2, left1);
assertGreaterThan(left1, left2);
}
@Test
public void testCompareEndsRightDownButDifferentRightColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> right1 = endSegment(e13, p, UP, RIGHT, DOWN);
ColumnSegment<TestEdge> right2 = endSegment(e14, p, UP, RIGHT_2, DOWN);
assertLessThan(right2, right1);
assertGreaterThan(right1, right2);
}
@Test
public void testCompareEndsRightUpButDifferentRightColumn() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> right1 = endSegment(e13, p, UP, RIGHT, UP);
ColumnSegment<TestEdge> right2 = endSegment(e14, p, UP, RIGHT_2, UP);
assertLessThan(right1, right2);
assertGreaterThan(right2, right1);
}
@Test
public void testCompareEndsLeftUpButUpperRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_u_left = endSegment(e13, p, UP, LEFT, UP, LEFT, UP);
ColumnSegment<TestEdge> l_u_right = endSegment(e14, p, UP, LEFT, UP, RIGHT, UP);
assertLessThan(l_u_left, l_u_right);
assertGreaterThan(l_u_right, l_u_left);
}
@Test
public void testCompareEndsLeftDownButLowerRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_d_left = endSegment(e13, p, UP, LEFT, DOWN, LEFT, UP);
ColumnSegment<TestEdge> l_d_right = endSegment(e14, p, UP, LEFT, DOWN, RIGHT, UP);
assertLessThan(l_d_right, l_d_left);
assertGreaterThan(l_d_left, l_d_right);
}
@Test
public void testCompareEndsRightUpButUpperRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_u_left = endSegment(e13, p, UP, RIGHT, UP, LEFT, UP);
ColumnSegment<TestEdge> r_u_right = endSegment(e14, p, UP, RIGHT, UP, RIGHT, UP);
assertLessThan(r_u_left, r_u_right);
assertGreaterThan(r_u_right, r_u_left);
}
@Test
public void testCompareEndsRightDownButLowerRowDifferentDirections() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_d_left = endSegment(e13, p, UP, RIGHT, DOWN, LEFT, UP);
ColumnSegment<TestEdge> r_d_right = endSegment(e14, p, UP, RIGHT, DOWN, RIGHT, UP);
assertLessThan(r_d_right, r_d_left);
assertGreaterThan(r_d_left, r_d_right);
}
@Test
public void testCompareEndsLeftUpLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_u_left = endSegment(e13, p, UP, LEFT, UP, LEFT, UP);
ColumnSegment<TestEdge> l_u_left2 = endSegment(e14, p, UP, LEFT, UP, LEFT_2, UP);
assertLessThan(l_u_left2, l_u_left);
assertGreaterThan(l_u_left, l_u_left2);
}
@Test
public void testCompareEndsLeftUpRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_u_right = endSegment(e13, p, UP, LEFT, UP, RIGHT, UP);
ColumnSegment<TestEdge> l_u_right2 = endSegment(e14, p, UP, LEFT, UP, RIGHT_2, UP);
assertLessThan(l_u_right, l_u_right2);
assertGreaterThan(l_u_right2, l_u_right);
}
@Test
public void testCompareEndsLeftDownLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_d_left = endSegment(e13, p, UP, LEFT, DOWN, LEFT, UP);
ColumnSegment<TestEdge> l_d_left2 = endSegment(e14, p, UP, LEFT, DOWN, LEFT_2, UP);
assertLessThan(l_d_left2, l_d_left);
assertGreaterThan(l_d_left, l_d_left2);
}
@Test
public void testCompareEndsLeftDownRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> l_d_right = endSegment(e13, p, UP, LEFT, DOWN, RIGHT, UP);
ColumnSegment<TestEdge> l_d_right2 = endSegment(e14, p, UP, LEFT, DOWN, RIGHT_2, UP);
assertLessThan(l_d_right, l_d_right2);
assertGreaterThan(l_d_right2, l_d_right);
}
@Test
public void testCompareEndsRightUpLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_u_left = endSegment(e13, p, UP, RIGHT, UP, LEFT, UP);
ColumnSegment<TestEdge> r_u_left2 = endSegment(e14, p, UP, RIGHT, UP, LEFT_2, UP);
assertLessThan(r_u_left2, r_u_left);
assertGreaterThan(r_u_left, r_u_left2);
}
@Test
public void testCompareEndsRightUpRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_u_right = endSegment(e13, p, UP, RIGHT, UP, RIGHT, UP);
ColumnSegment<TestEdge> r_u_right2 = endSegment(e14, p, UP, RIGHT, UP, RIGHT_2, UP);
assertLessThan(r_u_right, r_u_right2);
assertGreaterThan(r_u_right2, r_u_right);
}
@Test
public void testCompareEndsRightDownLeftButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_d_left = endSegment(e13, p, UP, RIGHT, DOWN, LEFT, UP);
ColumnSegment<TestEdge> r_d_left2 = endSegment(e14, p, UP, RIGHT, DOWN, LEFT_2, UP);
assertLessThan(r_d_left2, r_d_left);
assertGreaterThan(r_d_left, r_d_left2);
}
@Test
public void testCompareEndsRightDownRightButFinalColDiffers() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> r_d_right = endSegment(e13, p, UP, RIGHT, DOWN, RIGHT, UP);
ColumnSegment<TestEdge> r_d_right2 = endSegment(e14, p, UP, RIGHT, DOWN, RIGHT_2, UP);
assertLessThan(r_d_right, r_d_right2);
assertGreaterThan(r_d_right2, r_d_right);
}
//==================================================================================================
// The following tests compare interior edge segments are consistent with each other
//=================================================================================================
@Test
public void testInteriorRightDownRight() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, RIGHT, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, RIGHT, DOWN, RIGHT_2, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorRightDownLeft() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, RIGHT, DOWN, LEFT_2, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, RIGHT, DOWN, LEFT, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorRightUpRight() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, RIGHT, UP, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, RIGHT, UP, RIGHT_2, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg2, colSeg1);
assertGreaterThan(colSeg1, colSeg2);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorRightUpLeft() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, RIGHT, UP, LEFT_2, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, RIGHT, UP, LEFT, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg2, colSeg1);
assertGreaterThan(colSeg1, colSeg2);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorLeftDownRight() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, LEFT, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, LEFT, DOWN, RIGHT_2, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorLeftDownLeft() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, LEFT, DOWN, LEFT_2, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, LEFT, DOWN, LEFT, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorLeftUpRight() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, LEFT, UP, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, LEFT, UP, RIGHT_2, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg2, colSeg1);
assertGreaterThan(colSeg1, colSeg2);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg2, rowSeg1);
assertGreaterThan(rowSeg1, rowSeg2);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
@Test
public void testInteriorLeftUpLeft() {
GridPoint p = p(0, 0);
ColumnSegment<TestEdge> colSeg1 = segment(e13, p, DOWN, LEFT, UP, LEFT_2, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p, DOWN, LEFT, UP, LEFT, DOWN);
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
RowSegment<TestEdge> rowSeg1 = colSeg1.nextSegment();
RowSegment<TestEdge> rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg2, colSeg1);
assertGreaterThan(colSeg1, colSeg2);
rowSeg1 = colSeg1.nextSegment();
rowSeg2 = colSeg2.nextSegment();
assertLessThan(rowSeg1, rowSeg2);
assertGreaterThan(rowSeg2, rowSeg1);
colSeg1 = rowSeg1.nextSegment();
colSeg2 = rowSeg2.nextSegment();
assertLessThan(colSeg1, colSeg2);
assertGreaterThan(colSeg2, colSeg1);
}
//==================================================================================================
// The following tests are for miscellaneous methods in ColumnSegment or RowSegment
//=================================================================================================
@Test
public void testColumnSegmentOverlapColumn1TotallyAboveColumn2() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(100, 0), DOWN);
assertFalse(colSeg1.overlaps(colSeg2));
assertFalse(colSeg2.overlaps(colSeg1));
}
@Test
public void testColumnSegmentEndDoesNotOverlapColumnSegmentStartToSameVertex() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(2, 0), DOWN);
assertFalse(colSeg1.overlaps(colSeg2));
}
@Test
public void testColumnStartSegmentToSharedRowFromEndSegmentDependsOnRowOffsets() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 1), DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 0), DOWN, RIGHT, DOWN);
assertTrue(colSeg1.overlaps(colSeg2.last()));
// now fix offset for secondRow to be down a bit
colSeg2.nextSegment().setOffset(1);
assertFalse(colSeg1.overlaps(colSeg2.last()));
}
@Test
public void testRowSegmentOverlapSeg1TotallyBeforeSeg2() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 10), DOWN, RIGHT, DOWN);
assertFalse(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertFalse(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
@Test
public void testRowsLeavingSameVertexInOppositeDirectionsDontOverlap() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 0), DOWN, RIGHT, DOWN);
assertFalse(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertFalse(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
@Test
public void testRowsLeavingSameVertexInSameDirectionOverlap() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 0), DOWN, LEFT, DOWN);
assertTrue(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertTrue(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
@Test
public void testRowsEnteringSameVertexInOppositeDirectionsDontOverlap() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 2), DOWN, LEFT, DOWN);
assertFalse(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertFalse(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
@Test
public void testRowsEnteringSameVertexInSameDirectionsOverlap() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT_2, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 1), DOWN, RIGHT, DOWN);
assertTrue(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertTrue(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
@Test
public void testRowsThatStartEndOnSameColumnAndOneIsTerminalAndOtherIsnt() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT, DOWN, RIGHT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 1), DOWN, RIGHT, DOWN);
assertTrue(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertTrue(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
@Test
public void testRowsThatStartEndOnSameColumnAndOneIsTerminalAndOtherIsntLeft() {
ColumnSegment<TestEdge> colSeg1 = segment(e13, p(0, 1), DOWN, LEFT, DOWN, LEFT, DOWN);
ColumnSegment<TestEdge> colSeg2 = segment(e14, p(0, 0), DOWN, LEFT, DOWN);
assertTrue(colSeg1.nextSegment().overlaps(colSeg2.nextSegment()));
assertTrue(colSeg2.nextSegment().overlaps(colSeg1.nextSegment()));
}
private void assertCompareEquals(ColumnSegment<TestEdge> s1, ColumnSegment<TestEdge> s2) {
int result = s1.compareTo(s2);
if (result != 0) {
fail("Expected comparsion to be equals, but compareTo was " + result);
}
}
private void assertLessThan(ColumnSegment<TestEdge> s1, ColumnSegment<TestEdge> s2) {
int result = s1.compareTo(s2);
if (result >= 0) {
fail("Expected comparsion to be less than, but compareTo was " + result);
}
}
private void assertGreaterThan(RowSegment<TestEdge> s1, RowSegment<TestEdge> s2) {
int result = s1.compareTo(s2);
if (result <= 0) {
fail("Expected comparsion to be greater than, but compareTo was " + result);
}
}
private void assertLessThan(RowSegment<TestEdge> s1, RowSegment<TestEdge> s2) {
int result = s1.compareTo(s2);
if (result >= 0) {
fail("Expected comparsion to be less than, but compareTo was " + result);
}
}
private void assertGreaterThan(ColumnSegment<TestEdge> s1, ColumnSegment<TestEdge> s2) {
int result = s1.compareTo(s2);
if (result <= 0) {
fail("Expected comparsion to be greater than, but compareTo was " + result);
}
}
private ColumnSegment<TestEdge> segment(TestEdge e, GridPoint p, int... flows) {
return new ColumnSegment<>(e, points(p, flows));
}
private ColumnSegment<TestEdge> endSegment(TestEdge e, GridPoint end, int... flows) {
return new ColumnSegment<>(e, pointsReverseOrder(end, flows)).last();
}
private GridPoint p(int row, int col) {
return new GridPoint(row, col);
}
private List<GridPoint> points(GridPoint start, int... flows) {
List<GridPoint> points = new ArrayList<>();
points.add(start);
GridPoint next = new GridPoint(start.row, start.col);
for (int i = 0; i < flows.length; i++) {
if (i % 2 == 0) {
next.row += flows[i];
}
else {
next.col += flows[i];
}
points.add(new GridPoint(next.row, next.col));
}
return points;
}
private List<GridPoint> pointsReverseOrder(GridPoint end, int... flows) {
List<GridPoint> points = new ArrayList<>();
points.add(end);
GridPoint next = new GridPoint(end.row, end.col);
for (int i = 0; i < flows.length; i++) {
if (i % 2 == 0) {
next.row += flows[i];
}
else {
next.col += flows[i];
}
points.add(new GridPoint(next.row, next.col));
}
Collections.reverse(points);
return points;
}
private TestVertex v(int id) {
return new TestVertex(Integer.toString(id));
}
private TestEdge e(TestVertex vertex1, TestVertex vertex2) {
return new TestEdge(v1, v2);
}
}

View file

@ -171,9 +171,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
Address entryPoint = function.getEntryPoint();
FGVertex vertex = getVertex(jungGraph, entryPoint);
Integer row = gridLocations.row(vertex);
Integer col = gridLocations.col(vertex);
if (row != 0 || col != 0) {
GridPoint gridPoint = gridLocations.gridPoint(vertex);
if (gridPoint.row != 0 && gridPoint.col != 0) {
Msg.debug(this, "Function graph has entry point not at top of layout: " + entryPoint);
}
@ -338,7 +337,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
int startColumn = Math.min(start.columnIndex, end.columnIndex);
int endColumn = Math.max(start.columnIndex, end.columnIndex);
Column rightmostLoopColumn = layoutToGridMap.col(rightmostLoopVertex);
Column<FGVertex> rightmostLoopColumn = layoutToGridMap.col(rightmostLoopVertex);
endColumn = Math.max(endColumn, rightmostLoopColumn.index);
// Look for any vertices that are no part of the loop, but are placed inside
@ -351,8 +350,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
// place the right x position to the right of the rightmost vertex, not
// extending past the next column
FGVertex rightmostVertex = getRightmostVertex(interlopers);
Column rightmostColumn = layoutToGridMap.col(rightmostVertex);
Column nextColumn = layoutToGridMap.nextColumn(rightmostColumn);
Column<FGVertex> rightmostColumn = layoutToGridMap.col(rightmostVertex);
Column<FGVertex> nextColumn = layoutToGridMap.nextColumn(rightmostColumn);
Vertex2d rightmostV2d = vertex2dFactory.get(rightmostVertex);
// the padding used for these two lines is somewhat arbitrary and may be changed
@ -646,7 +645,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
}
else {
// going up we swing out to the right; grab the column that is out to the right
Column rightColumn = vertex2dFactory.getColumn(edgeX);
Column<FGVertex> rightColumn = vertex2dFactory.getColumn(edgeX);
endColumn = rightColumn.index;
}
@ -855,7 +854,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
}
@Override
protected Point2D getVertexLocation(FGVertex v, Column col, Row<FGVertex> row,
protected Point2D getVertexLocation(FGVertex v, Column<FGVertex> col, Row<FGVertex> row,
Rectangle bounds) {
return getCenteredVertexLocation(v, col, row, bounds);
}
@ -1128,7 +1127,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
this.edgeOffset = edgeOffset;
}
Column getColumn(double x) {
Column<FGVertex> getColumn(double x) {
return layoutToGridMap.getColumnContaining((int) x);
}
@ -1163,7 +1162,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
private FGVertex v;
private Row<FGVertex> row;
private Column column;
private Column<FGVertex> column;
private int rowIndex;
private int columnIndex;
private Point2D center; // center point of vertex shape

View file

@ -63,7 +63,7 @@ public class BowTieLayout extends AbstractVisualGraphLayout<FcgVertex, FcgEdge>
}
@Override
protected Point2D getVertexLocation(FcgVertex v, Column col, Row<FcgVertex> row,
protected Point2D getVertexLocation(FcgVertex v, Column<FcgVertex> col, Row<FcgVertex> row,
Rectangle bounds) {
return getCenteredVertexLocation(v, col, row, bounds);
}

View file

@ -485,4 +485,21 @@ public class CollectionUtils {
}
return null;
}
/**
* Returns the only element from the given collection; null if the collection is null or empty
* or size is greater than 1. This is meant to clients to get the one and only element in
* a collection of size 1.
*
* @param c the collection
* @return the item
* @see #any(Collection)
*/
public static <T> T get(Collection<T> c) {
if (c == null || c.size() > 1) {
return null;
}
return any((Iterable<T>) c);
}
}

View file

@ -627,4 +627,48 @@ public class GraphAlgorithms {
recursivePrint(g, v2, set, depth + 1, ps);
}
}
/**
* Returns a list of list of vertices sorted topologically such that for every edge
* V1 -> V2, the V1 vertex will appear in the resulting list before the V2 vertex. Normally,
* this is only defined for acyclic graphs. For purposes of this implementation, a root vertex
* is given as a start point and any edge encountered by following edges from the root that
* results in a "back" edge (i.e any edge that points to a previously visited vertex) is
* ignored, effectively making the graph acyclic (somewhat arbitrarily depending the order in
* which vertexes are visited which is determined by the given edge comparator). Also, note
* that any vertex in the graph that is not reachable from the given root will not appear in
* the resulting list of sorted vertices.
*
* @param <V> the vertex type
* @param <E> the edge type
* @param g the graph
* @param root the start node for traversing the graph (will always be the first node in the
* resulting list)
* @param edgeComparator provides an ordering for traversing the graph which can impact which
* edges are ignored as "back" edges and ultimately affect the final ordering
* @return a list of vertices reachable from the given root vertex, sorted topologically
*/
public static <V, E extends GEdge<V>> List<V> topologicalSort(GDirectedGraph<V, E> g, V root,
Comparator<E> edgeComparator) {
GraphToTreeAlgorithm<V, E> algorithm = new GraphToTreeAlgorithm<V, E>(g, edgeComparator);
return algorithm.topolocigalSort(root);
}
/**
* Converts a general directed graph into a tree graph with the given vertex as the root. It
* does this by first doing a topological sort (which ignores back edges) and greedily accepting
* the first incoming edge based on the sorted vertex order.
* @param <V> the vertex type
* @param <E> the edge type
* @param g the graph to be converted into a tree
* @param root the vertex to be used as the root
* @param edgeComparator provides a priority ordering of edges with higher priority edges
* getting first shot at claiming children for its sub-tree.
* @return a graph with edges removed such that the graph is a tree.
*/
public static <V, E extends GEdge<V>> GDirectedGraph<V, E> toTree(GDirectedGraph<V, E> g,
V root, Comparator<E> edgeComparator) {
GraphToTreeAlgorithm<V, E> algorithm = new GraphToTreeAlgorithm<V, E>(g, edgeComparator);
return algorithm.toTree(root);
}
}

View file

@ -0,0 +1,216 @@
/* ###
* 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 java.util.*;
import org.apache.commons.collections4.map.LazyMap;
import ghidra.graph.jung.JungDirectedGraph;
/**
* This class provides an algorithm for topological graph sorting and an algorithm for using
* that topological sort to create a tree structure from the graph using that topological sort.
* <P>
* In general topological sorting and converting to a tree, require an acyclic graph. However,
* by supplying a root vertex, the graph can be made to be acyclic by traversing the graph from
* that root and discarding any edges the return to a "visited" vertex. This has a side effect of
* ignoring any nodes that are not reachable from the root node. Also, this algorithm class is
* constructed with an edge comparator which can also determine the order nodes are traversed,
* thereby affecting the final ordering or tree structure. Higher priority edges will be processed
* first, making those edges least likely to be removed as "back" edges.
* <P>
* To convert a general graph to a tree, some subset of the the graphs original edges are used to
* form the tree. There are many possible different trees that can be created in this way. This
* algorimth's goal is to create a tree such that if all the original "forward" edges are added
* back to the tree, they only flow down the tree. This is useful for creating a nicely organized
* layout of vertices and edges when drawn.
*
* @param <V> The vertex type
* @param <E> The edge type
*/
public class GraphToTreeAlgorithm<V, E extends GEdge<V>> {
private GDirectedGraph<V, E> graph;
private Comparator<E> edgeComparator;
/**
* Constructor.
*
* @param graph the graph from with to create a tree
* @param edgeComparator provides a priority ordering of edges with higher priority edges
* getting first shot at claiming children for its sub-tree.
*/
public GraphToTreeAlgorithm(GDirectedGraph<V, E> graph, Comparator<E> edgeComparator) {
this.graph = graph;
this.edgeComparator = edgeComparator;
}
/**
* Creates a tree graph with the given vertex as the root from this object's graph.
*
* @param root the vertex to be used as the root
* getting first shot at claiming children for its sub-tree.
* @return a graph with edges removed such that the graph is a tree.
*/
public GDirectedGraph<V, E> toTree(V root) {
// first sort the vertices topologically
List<V> sorted = topolocigalSort(root);
// Visit nodes in the sorted order and track the longest path to each node from the root.
Map<V, Depth> depthMap = assignDepths(root, sorted);
// Assign vertices to the tree in the sorted order and only using edges where the "from"
// vertex (parent) is at a depth 1 less then the depth of "to" vertex. This will ensure
// that the tree is ordered such that if all the original forward edges are added back in,
// they would always flow down the tree.
return createTree(root, sorted, depthMap);
}
/**
* Sorts the vertices in this graph topologically.
*
* @param root the start node for traversing the graph (will always be the first node in the
* resulting list)
* @return a list of vertices reachable from the given root vertex, sorted topologically
*/
public List<V> topolocigalSort(V root) {
Set<V> visited = new HashSet<>();
List<V> ordered = new ArrayList<>();
Deque<VertexChildIterator> stack = new ArrayDeque<>();
stack.push(new VertexChildIterator(root));
visited.add(root);
while (!stack.isEmpty()) {
VertexChildIterator childIterator = stack.getFirst();
if (childIterator.hasNext()) {
V child = childIterator.next();
// only process the child if never seen before, otherwise it is a loop back
if (!visited.contains(child)) {
stack.push(new VertexChildIterator(child));
visited.add(child);
}
}
else {
ordered.add(childIterator.getParent());
stack.pop();
}
}
Collections.reverse(ordered);
return ordered;
}
private JungDirectedGraph<V, E> createTree(V root, List<V> sorted, Map<V, Depth> depthMap) {
Set<V> visited = new HashSet<>();
visited.add(root);
JungDirectedGraph<V, E> tree = new JungDirectedGraph<V, E>();
for (V v : sorted) {
tree.addVertex(v);
}
for (V parent : sorted) {
Depth parentDepth = depthMap.get(parent);
Collection<E> outEdges = graph.getOutEdges(parent);
for (E e : outEdges) {
V child = e.getEnd();
if (visited.contains(child)) {
continue; // already assigned
}
Depth childDepth = depthMap.get(child);
if (childDepth.isDirectChildOf(parentDepth)) {
tree.addEdge(e);
visited.add(child);
}
}
}
return tree;
}
private Map<V, Depth> assignDepths(V root, List<V> sorted) {
Set<V> visited = new HashSet<>();
Map<V, Depth> depthMap = LazyMap.lazyMap(new HashMap<>(), k -> new Depth());
depthMap.put(root, new Depth());
for (V parent : sorted) {
visited.add(parent);
Depth parentDepth = depthMap.get(parent);
List<E> edges = new ArrayList<>();
Collection<E> out = graph.getOutEdges(parent);
if (out != null) {
edges.addAll(out);
}
edges.sort(edgeComparator);
for (E e : edges) {
V child = e.getEnd();
if (visited.contains(child)) {
continue; // loop backs are ignored
}
Depth childDepth = depthMap.get(child);
childDepth.adjustDepth(parentDepth);
}
}
return depthMap;
}
// traces the distance from the root of the tree
private static class Depth {
private int depth = 0;
private void adjustDepth(Depth parentDepth) {
depth = Math.max(depth, parentDepth.depth + 1);
}
private boolean isDirectChildOf(Depth parentDepth) {
return depth == parentDepth.depth + 1;
}
}
private class VertexChildIterator {
private V parent;
private Iterator<E> it;
VertexChildIterator(V parent) {
this.parent = parent;
Collection<E> out = graph.getOutEdges(parent);
List<E> outEdges = new ArrayList<>();
if (out != null) {
outEdges.addAll(out);
}
outEdges.sort(edgeComparator);
it = outEdges.reversed().iterator();
}
V getParent() {
return parent;
}
public boolean hasNext() {
return it.hasNext();
}
public V next() {
return it.next().getEnd();
}
}
}

View file

@ -627,7 +627,7 @@ public class GraphComponent<V extends VisualVertex, E extends VisualEdge<V>, G e
return satelliteViewer;
}
protected VisualGraphViewUpdater<V, E> getViewUpdater() {
public VisualGraphViewUpdater<V, E> getViewUpdater() {
return primaryViewer.getViewUpdater();
}

View file

@ -335,7 +335,8 @@ public class VisualGraphViewUpdater<V extends VisualVertex, E extends VisualEdge
setGraphScale(graphInfo.getZoom());
Point layoutPoint = graphInfo.getLayoutTranslateCoordinates();
multiLayerTransformer.getTransformer(Layer.LAYOUT).setTranslate(layoutPoint.x,
multiLayerTransformer.getTransformer(Layer.LAYOUT)
.setTranslate(layoutPoint.x,
layoutPoint.y);
Point viewPoint = graphInfo.getViewTranslateCoordinates();
@ -443,4 +444,8 @@ public class VisualGraphViewUpdater<V extends VisualVertex, E extends VisualEdge
stopVertexTwinkleAnimation();
}
public void relayoutGraph() {
scheduleViewChangeJob(new RelayoutFunctionGraphJob<>(primaryViewer, isAnimationEnabled()));
}
}

View file

@ -337,7 +337,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
}
// DEGUG triggers grid lines to be printed; useful for debugging
// VisualGraphRenderer.DEBUG_ROW_COL_MAP.put(this, layoutLocations.copy());
// VisualGraphRenderer.setGridPainter(new GridPainter(layoutLocations.getGridCoordinates()));
layoutLocations.dispose();
gridLocations.dispose();
@ -356,7 +356,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
monitor.checkCancelled();
Row<V> row = layoutLocations.row(vertex);
Column column = layoutLocations.col(vertex);
Column<V> column = layoutLocations.col(vertex);
Shape shape = transformer.apply(vertex);
Rectangle bounds = shape.getBounds();
@ -366,7 +366,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
return newLocations;
}
protected Point2D getVertexLocation(V v, Column col, Row<V> row, Rectangle bounds) {
protected Point2D getVertexLocation(V v, Column<V> col, Row<V> row, Rectangle bounds) {
int x = col.x - bounds.x;
int y = row.y - bounds.y;
return new Point2D.Double(x, y);
@ -381,7 +381,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
* @param bounds the bounds of the vertex in the layout space
* @return the centered location
*/
protected Point2D getCenteredVertexLocation(V v, Column col, Row<V> row, Rectangle bounds) {
protected Point2D getCenteredVertexLocation(V v, Column<V> col, Row<V> row, Rectangle bounds) {
//
// Move x over to compensate for vertex painting. Edges are drawn from the center of the
// vertex. Thus, if you have vertices with two different widths, then the edge between
@ -409,9 +409,9 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
monitor.checkCancelled();
List<Point2D> newArticulations = new ArrayList<>();
for (Point gridPoint : layoutLocations.articulations(edge)) {
Row<V> row = layoutLocations.row(gridPoint.y);
Column column = layoutLocations.col(gridPoint.x);
for (GridPoint gridPoint : layoutLocations.articulations(edge)) {
Row<V> row = layoutLocations.row(gridPoint.row);
Column<V> column = layoutLocations.col(gridPoint.col);
Point2D location = getEdgeLocation(column, row);
newArticulations.add(location);
@ -421,11 +421,11 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
return newEdgeArticulations;
}
protected Point2D getEdgeLocation(Column col, Row<V> row) {
protected Point2D getEdgeLocation(Column<V> col, Row<V> row) {
return new Point2D.Double(col.x, row.y);
}
protected Point2D getCenteredEdgeLocation(Column col, Row<V> row) {
protected Point2D getCenteredEdgeLocation(Column<V> col, Row<V> row) {
//
// half-height offsets the articulation points, which keeps long edge lines from
// overlapping as much

View file

@ -16,16 +16,22 @@
package ghidra.graph.viewer.layout;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.Map.Entry;
import ghidra.graph.viewer.GraphViewerUtils;
/**
* A row in a grid. This class stores it's row index, its x offset and its width. The
* A column in a grid. This class stores its column index, its x offset and its width. The
* x value is the layout space x value of a {@link Point2D} object. That is, unlike the
* {@link GridLocationMap}, the x value of this object is in layout space and not indexes
* of a grid.
*
* <p>This class maintains a collection of vertices on this column, organized by column index. You
* can get the column of a vertex from {@link #getRow(Object)}.
* @param <V> The vertex type
*/
public class Column {
public class Column<V> {
/** The <b>layout</b> x coordinate of the column */
public int x = -1;
@ -33,11 +39,26 @@ public class Column {
/** The grid index of this column (0, 1...n) for the number of columns */
public int index = Integer.MAX_VALUE;
// Note: these must change together (they are effectively a BiDi map)
private TreeMap<Integer, V> verticesByRow = new TreeMap<>();
private Map<V, Integer> rowsByVertex = new HashMap<>();
public Column(int index) {
this.index = index;
}
public void setRow(V v, int row) {
rowsByVertex.put(v, row);
verticesByRow.put(row, v);
}
public int getRow(V v) {
if (!rowsByVertex.containsKey(v)) {
throw new IllegalArgumentException("Vertex is not in row: " + v);
}
return rowsByVertex.get(v);
}
public int getPaddedWidth(boolean isCondensed) {
if (isCondensed) {
return width + GraphViewerUtils.EXTRA_LAYOUT_COLUMN_SPACING_CONDENSED;
@ -61,4 +82,13 @@ public class Column {
"}";
//@formatter:on
}
public boolean isOpenBetween(int startRow, int endRow) {
Entry<Integer, V> ceilingEntry = verticesByRow.ceilingEntry(startRow);
if (ceilingEntry == null) {
return true;
}
int nextRow = ceilingEntry.getKey();
return nextRow > endRow;
}
}

View file

@ -0,0 +1,100 @@
/* ###
* 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.viewer.layout;
/**
* Tracks the minimum and maximum indexes for both rows and columns.
*/
public class GridBounds {
private int minRow = 0;
private int maxRow = 0;
private int minCol = 0;
private int maxCol = 0;
/**
* Updates the bounds for the given GridPoint.
* @param p the gridPoint used to update the minimums and maximums
*/
public void update(GridPoint p) {
minRow = Math.min(minRow, p.row);
maxRow = Math.max(maxRow, p.row);
minCol = Math.min(minCol, p.col);
maxCol = Math.max(maxCol, p.col);
}
/**
* Shifts the columns bounds by the given amount
* @param rowShift the amount to shift the row bounds.
* @param colShift the amount to shift the column bounds.
* @throws IllegalArgumentException if the shift would make the minimum column negative
*/
public void shift(int rowShift, int colShift) {
minCol += colShift;
maxCol += colShift;
minRow += rowShift;
maxRow += rowShift;
}
@Override
public String toString() {
StringBuilder buffy = new StringBuilder();
buffy.append("Grid Bounds: ");
if (minRow == Integer.MAX_VALUE) {
return "Empty";
}
buffy.append("rows: ").append(minRow).append(" -> ").append(maxRow);
buffy.append(", ");
buffy.append("cols: ").append(minCol).append(" -> ").append(maxCol);
return buffy.toString();
}
public int maxCol() {
return maxCol;
}
public int minCol() {
// handle case when grid is empty
if (minCol > maxCol) {
return 0;
}
return minCol;
}
public int maxRow() {
return maxRow;
}
public int minRow() {
// handle case when grid is empty
if (minRow > maxRow) {
return 0;
}
return minRow;
}
public boolean contains(GridPoint p) {
if (p.row < minRow || p.row > maxRow) {
return false;
}
if (p.col < minCol || p.col > maxCol) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,79 @@
/* ###
* 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.viewer.layout;
import java.awt.Rectangle;
/**
* Tracks the mapping of grid coordinates (rows, columns) to space coordinates (x, y)
*/
public class GridCoordinates {
private int[] rowStarts;
private int[] colStarts;
/**
* Constructor
* @param rowCoordinates an array containing the y locations for all rows in a grid
* @param columnCoordinates an array containing the x locations for all columns in a grid
*/
public GridCoordinates(int[] rowCoordinates, int[] columnCoordinates) {
rowStarts = rowCoordinates;
colStarts = columnCoordinates;
}
/**
* Returns the x value for a given column.
* @param col the column index in the grid
* @return the x coordinate assigned to the given column index
*/
public int x(int col) {
return colStarts[col];
}
/**
* Returns the y value for a given row.
* @param row the row index in the grid
* @return the y coordinate assigned to the given row index
*/
public int y(int row) {
return rowStarts[row];
}
/**
* Returns the total bounds for the grid
* @return the total bounds for the grid
*/
public Rectangle getBounds() {
return new Rectangle(0, 0, colStarts[colStarts.length - 1],
rowStarts[rowStarts.length - 1]);
}
/**
* returns the number of rows in the grid.
* @return the number of rows in the grid
*/
public int rowCount() {
return rowStarts.length;
}
/**
* returns the number of columns in the grid.
* @return the number of columns in the grid
*/
public int columnCount() {
return colStarts.length;
}
}

View file

@ -15,49 +15,86 @@
*/
package ghidra.graph.viewer.layout;
import java.awt.Point;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Stream;
import org.apache.commons.collections4.Factory;
import org.apache.commons.collections4.map.LazyMap;
/**
* An object that maps vertices to rows and columns and edges to their articulation points.
* This class is essentially a container that allows layout algorithms to store results, which
* can later be turned into layout positioning points. The integer point values in this
* class are row, column grid values, starting at 0,0.
*
* <P>Note: the Point2D values for the edge articulations use x,y values that are row and
* column index values, the same values as calling {@link #row(Object) row(V)} and {@link #col(Object) col(V)}.
*
* <P>After building the grid using this class, clients can call {@link #rows()} to get
* high-order object that represent rows.
* An object that maps vertices and edge articulation points to rows and columns in a grid. This
* class is essentially a container that allows layout algorithms to store results as it lays
* out vertices and edges in a virtual grid. Later, this information can be used in conjunction
* with vertex size information and padding information to transform these grid coordinates to
* layout space coordinates.
* <P>
* This object also has methods for manipulating the grid such as shifting it up, down, left, right,
* and merging in other GridLocationMaps
* <P>
* After building the grid using this class, clients can call {@link #rows()}, {@link #rowsMap()},
* or {@link #columnsMap()} to get high-order objects that represent rows or columns.
*
* @param <V> the vertex type
* @param <E> the edge type
*/
public class GridLocationMap<V, E> {
private Factory<Point> rowColFactory = () -> new Point();
protected Map<V, GridPoint> vertexPoints =
LazyMap.lazyMap(new HashMap<V, GridPoint>(), v -> new GridPoint(0, 0));
private Map<V, Point> vertexPoints = LazyMap.lazyMap(new HashMap<V, Point>(), rowColFactory);
private Map<E, List<Point>> edgePoints = new HashMap<>();
protected Map<E, List<GridPoint>> edgePoints = new HashMap<>();
private GridBounds gridBounds = new GridBounds();
// Tree based algorithms might want to track the column of the root node as it changes when
// the grid is shifted or merged.Useful for determining the position of a parent node when
// building bottom up.
private int rootColumn = 0;
Set<V> vertices() {
public GridLocationMap() {
rootColumn = 0;
}
/**
* Constructor that includes an initial "root" vertex.
* @param root the initial vertex
* @param row the row for the initial vertex
* @param col the column for the initial vertex.
*/
public GridLocationMap(V root, int row, int col) {
this.rootColumn = col;
set(root, new GridPoint(row, col));
}
/**
* Returns the column of the initial vertex in this grid.
* @return the column of the initial vertex in this grid
*/
public int getRootColumn() {
return rootColumn;
}
public Set<V> vertices() {
return vertexPoints.keySet();
}
Set<E> edges() {
public Set<E> edges() {
return edgePoints.keySet();
}
public void setArticulations(E edge, List<Point> articulations) {
edgePoints.put(edge, articulations);
public Map<V, GridPoint> getVertexPoints() {
return vertexPoints;
}
public List<Point> getArticulations(E edge) {
List<Point> list = edgePoints.get(edge);
public void setArticulations(E edge, List<GridPoint> articulations) {
edgePoints.put(edge, articulations);
if (articulations != null) {
for (GridPoint gridPoint : articulations) {
gridBounds.update(gridPoint);
}
}
}
public List<GridPoint> getArticulations(E edge) {
List<GridPoint> list = edgePoints.get(edge);
if (list == null) {
return Collections.emptyList();
}
@ -65,25 +102,44 @@ public class GridLocationMap<V, E> {
}
public void row(V vertex, int row) {
vertexPoints.get(vertex).y = row;
GridPoint gridPoint = vertexPoints.get(vertex);
gridPoint.row = row;
gridBounds.update(gridPoint);
}
public void col(V vertex, int col) {
vertexPoints.get(vertex).x = col;
GridPoint gridPoint = vertexPoints.get(vertex);
gridPoint.col = col;
gridBounds.update(gridPoint);
}
public void set(V v, int row, int col) {
Point p = vertexPoints.get(v);
p.x = col;
p.y = row;
set(v, new GridPoint(row, col));
}
public void set(V v, GridPoint gridPoint) {
vertexPoints.put(v, gridPoint);
gridBounds.update(gridPoint);
}
public int row(V vertex) {
return vertexPoints.get(vertex).y;
GridPoint gridPoint = vertexPoints.get(vertex);
if (gridPoint != null) {
return gridPoint.row;
}
return 0;
}
public int col(V vertex) {
return vertexPoints.get(vertex).x;
GridPoint gridPoint = vertexPoints.get(vertex);
if (gridPoint != null) {
return gridPoint.col;
}
return 0;
}
public GridPoint gridPoint(V vertex) {
return vertexPoints.get(vertex);
}
/**
@ -92,143 +148,82 @@ public class GridLocationMap<V, E> {
* @return the rows in this grid
*/
public List<Row<V>> rows() {
Map<Integer, Row<V>> rowsByIndex = new HashMap<>();
Set<Entry<V, Point>> entrySet = vertexPoints.entrySet();
for (Entry<V, Point> entry : entrySet) {
V v = entry.getKey();
Point gridPoint = entry.getValue();
int rowIndex = gridPoint.y;
Row<V> row = getRow(rowsByIndex, rowIndex);
row.index = rowIndex;
row.setColumn(v, gridPoint.x);
}
Map<Integer, Row<V>> rowsByIndex = rowsMap();
List<Row<V>> rows = new ArrayList<>(rowsByIndex.values());
rows.sort((r1, r2) -> r1.index - r2.index);
return rows;
}
private Row<V> getRow(Map<Integer, Row<V>> rows, int rowIndex) {
Row<V> row = rows.get(rowIndex);
if (row == null) {
row = new Row<>(rowIndex);
rows.put(rowIndex, row);
/**
* Returns a mapping or row indexes to Row objects in this grid
*
* @return the rows in this grid
*/
public Map<Integer, Row<V>> rowsMap() {
Map<Integer, Row<V>> rowsByIndex = LazyMap.lazyMap(new HashMap<>(), r -> new Row<V>(r));
Set<Entry<V, GridPoint>> entrySet = vertexPoints.entrySet();
for (Entry<V, GridPoint> entry : entrySet) {
V v = entry.getKey();
GridPoint gridPoint = entry.getValue();
int rowIndex = gridPoint.row;
Row<V> row = rowsByIndex.get(rowIndex);
row.setColumn(v, gridPoint.col);
}
return row;
return rowsByIndex;
}
/**
* Updates each row within the grid such that it's x values are set to center the row in
* Returns a mapping or column indexes to Column objects in this grid
*
* @return the columns in this grid
*/
public Map<Integer, Column<V>> columnsMap() {
Map<Integer, Column<V>> columnsMap =
LazyMap.lazyMap(new HashMap<>(), c -> new Column<V>(c));
Set<Entry<V, GridPoint>> entrySet = vertexPoints.entrySet();
for (Entry<V, GridPoint> entry : entrySet) {
V v = entry.getKey();
GridPoint gridPoint = entry.getValue();
int colIndex = gridPoint.col;
Column<V> col = columnsMap.get(colIndex);
col.setRow(v, gridPoint.row);
}
return columnsMap;
}
/**
* Updates each row within the grid such that it's column values are set to center the row in
* the grid. Each row will be updated so that all its columns start at zero. After that,
* each column will be centered in the grid.
*/
public void centerRows() {
zeroAlignGrid();
GridRange[] vertexColumnRanges = getVertexColumnRanges();
int maxRowWidth = getMaxRowWidth(vertexColumnRanges);
List<Row<V>> rows = rows();
int maxCol = columnCount(rows);
for (Row<V> row : rows) {
row = zeroRowColumns(row);
int rowColumnCount = row.getColumnCount();
if (rowColumnCount == maxCol) {
continue; // already the full size; no need to center
}
int delta = maxCol - rowColumnCount;
int offset = delta / 2;
List<V> vertices = row.getVertices();
for (V v : vertices) {
if (v == null) {
continue;
}
int oldCol = col(v);
set(v, row.index, oldCol + offset);
}
row.dispose();
for (GridPoint p : allPoints()) {
GridRange range = vertexColumnRanges[p.row];
int extraSpace = maxRowWidth - range.width();
int shift = extraSpace / 2 - range.min;
p.col += shift;
}
}
private int maxColumnIndex(List<Row<V>> rows) {
int maxCol = 0;
for (Row<V> row : rows) {
maxCol = Math.max(maxCol, row.getEndColumn());
private int getMaxRowWidth(GridRange[] vertexColumnRanges) {
int maxWidth = 0;
for (GridRange gridRange : vertexColumnRanges) {
maxWidth = Math.max(maxWidth, gridRange.width());
}
return maxCol;
return maxWidth;
}
private int maxRowIndex(List<Row<V>> rows) {
int maxRow = 0;
for (Row<V> row : rows) {
maxRow = Math.max(maxRow, row.index);
}
return maxRow;
}
private int columnCount(List<Row<V>> rows) {
int maxCount = 0;
for (Row<V> row : rows) {
maxCount = Math.max(maxCount, row.getColumnCount());
}
return maxCount;
}
// private int rowCount(List<Row<V>> rows) {
// int minRow = 0;
// int maxRow = 0;
// for (Row<V> row : rows) {
// minRow = Math.min(minRow, row.index);
// maxRow = Math.max(maxRow, row.index);
// }
// return (maxRow - minRow) + 1; // +1 for zero-based
// }
private Row<V> zeroRowColumns(Row<V> row) {
int start = row.getStartColumn();
int offset = -start;
Row<V> updatedRow = new Row<>();
updatedRow.index = row.index;
for (V v : row.getVertices()) {
int oldCol = col(v);
int newCol = oldCol + offset;
set(v, row.index, newCol);
updatedRow.setColumn(v, newCol);
}
row.dispose();
return updatedRow;
}
GridLocationMap<V, E> copy() {
GridLocationMap<V, E> map = new GridLocationMap<>();
map.vertexPoints = new HashMap<>();
Set<Entry<V, Point>> entries = vertexPoints.entrySet();
for (Entry<V, Point> entry : entries) {
map.vertexPoints.put(entry.getKey(), (Point) entry.getValue().clone());
}
map.edgePoints = new HashMap<>();
Set<Entry<E, List<Point>>> edgeEntries = edgePoints.entrySet();
for (Entry<E, List<Point>> entry : edgeEntries) {
List<Point> points = entry.getValue();
List<Point> clonedPoints = new ArrayList<>(points.size());
for (Point p : points) {
clonedPoints.add((Point) p.clone());
}
map.edgePoints.put(entry.getKey(), clonedPoints);
}
return map;
/**
* Shifts the grid so that its first row and column are at 0.
*/
public void zeroAlignGrid() {
shift(-gridBounds.minRow(), -gridBounds.minCol());
}
public void dispose() {
@ -236,6 +231,103 @@ public class GridLocationMap<V, E> {
edgePoints.clear();
}
/**
* Shifts the rows and columns for all points in this map by the given amount.
* @param rowShift the amount to shift the rows of each point
* @param colShift the amount to shift the columns of each point
*/
public void shift(int rowShift, int colShift) {
if (rowShift == 0 && colShift == 0) {
return;
}
for (GridPoint p : allPoints()) {
p.row += rowShift;
p.col += colShift;
}
rootColumn += colShift;
gridBounds.shift(rowShift, colShift);
}
/**
* Returns the number of rows in this grid map. Note that this includes empty rows
* starting at the 0 row.
* @return the number of rows in this grid map
*/
public int height() {
return gridBounds.maxRow() + 1;
}
/**
* Returns the number of columns in this grid map. Note that this includes empty columns
* starting at the 0 column.
* @return the number of columns in this grid map
*/
public int width() {
return gridBounds.maxCol() + 1;
}
/**
* Returns the minimum/max column for all rows in the grid. This method is only defined for
* grids that have no negative rows. This is because the array returned will be 0 based, with
* the entry at index 0 containing the column bounds for row 0 and so on.
* @return the minimum/max column for all rows in the grid
* @throws IllegalStateException if this method is called on a grid with negative rows.
*/
public GridRange[] getVertexColumnRanges() {
if (gridBounds.minRow() < 0) {
throw new IllegalStateException(
"getVertexColumnRanges not defined for grids with negative rows!");
}
GridRange[] rowRanges = new GridRange[height()];
for (int i = 0; i < rowRanges.length; i++) {
rowRanges[i] = new GridRange();
}
for (GridPoint p : vertexPoints.values()) {
rowRanges[p.row].add(p.col);
}
return rowRanges;
}
public boolean containsVertex(V v) {
return vertexPoints.containsKey(v);
}
public boolean containsEdge(E e) {
return edgePoints.containsKey(e);
}
/**
* Adds in the vertices and edges from another GridLocationMap with each point in the other
* grid map shifted by the given row and column amounts.
* @param other the other GridLocationMap to add to this one.
* @param rowShift the amount to shift the rows in the grid points from the other grid before
* adding them to this grid
* @param colShift the amount to shift the columns in the grid points from the other grid before
* adding them to this grid
*/
public void add(GridLocationMap<V, E> other, int rowShift, int colShift) {
for (Entry<V, GridPoint> entry : other.vertexPoints.entrySet()) {
V v = entry.getKey();
GridPoint point = entry.getValue();
set(v, new GridPoint(point.row + rowShift, point.col + colShift));
}
for (Entry<E, List<GridPoint>> entry : other.edgePoints.entrySet()) {
E e = entry.getKey();
List<GridPoint> points = entry.getValue();
List<GridPoint> shiftedPoints = new ArrayList<>(points.size());
for (GridPoint point : points) {
shiftedPoints.add(new GridPoint(point.row + rowShift, point.col + colShift));
}
setArticulations(e, shiftedPoints);
}
}
@Override
public String toString() {
return getClass().getSimpleName() + "[\n\tvertex points=" + vertexPoints +
@ -247,66 +339,71 @@ public class GridLocationMap<V, E> {
* @return a string representation of this grid
*/
public String toStringGrid() {
int minRow = gridBounds.minRow();
int minCol = gridBounds.minCol();
if (minRow > 10 || minCol > 10) {
GridLocationMap<V, E> copy = copy();
zeroAlignGrid(copy);
List<Row<V>> rows = copy.rows();
int columnCount = copy.maxColumnIndex(rows) + 1;
int rowCount = copy.maxRowIndex(rows) + 1;
Object[][] vGrid = new Object[rowCount][columnCount];
for (Row<V> row : rows) {
List<V> vertices = row.getVertices();
for (V v : vertices) {
vGrid[row.index][row.getColumn(v)] = v;
}
copy.zeroAlignGrid();
return "grid upper left (row,col) = (" + minRow + ", " + minCol + ")\n" +
copy.toStringGrid();
}
StringBuilder buffy = new StringBuilder("\n");
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
Object o = vGrid[row][col];
buffy.append(' ');
if (o == null) {
buffy.append('-');
//buffy.append(' ');
}
else {
buffy.append('v');
}
buffy.append(' ');
}
buffy.append('\n');
}
String[][] vGrid = new String[height()][width()];
for (Entry<V, GridPoint> entry : vertexPoints.entrySet()) {
V v = entry.getKey();
GridPoint p = entry.getValue();
vGrid[p.row][p.col] = normalizeVertexName(v.toString());
}
StringBuilder buffy = new StringBuilder();
buffy.append("\n");
for (int row = 0; row < vGrid.length; row++) {
for (int col = 0; col < vGrid[row].length; col++) {
String name = vGrid[row][col];
name = name == null ? ". " : name;
buffy.append(name);
buffy.append("");
}
buffy.append("\n");
}
return buffy.toString();
}
// moves all rows and columns as needed to convert the grid origin to 0,0
private static <V, E> void zeroAlignGrid(GridLocationMap<V, E> grid) {
private GridLocationMap<V, E> copy() {
GridLocationMap<V, E> map = new GridLocationMap<>();
map.rootColumn = rootColumn;
int smallestColumnIndex = 0;
int smallestRowIndex = 0;
List<Row<V>> rows = grid.rows();
for (Row<V> row : rows) {
smallestRowIndex = Math.min(smallestRowIndex, row.index);
smallestColumnIndex = Math.min(smallestColumnIndex, row.getStartColumn());
Set<Entry<V, GridPoint>> entries = vertexPoints.entrySet();
for (Entry<V, GridPoint> entry : entries) {
map.set(entry.getKey(), new GridPoint(entry.getValue()));
}
int globalColumnOffset = -smallestColumnIndex;
int globalRowOffset = -smallestRowIndex;
for (Row<V> row : rows) {
List<V> vertices = row.getVertices();
for (V v : vertices) {
int oldCol = grid.col(v);
int oldRow = grid.row(v);
int newCol = globalColumnOffset + oldCol;
int newRow = globalRowOffset + oldRow;
grid.set(v, newRow, newCol);
Set<Entry<E, List<GridPoint>>> edgeEntries = edgePoints.entrySet();
for (Entry<E, List<GridPoint>> entry : edgeEntries) {
List<GridPoint> points = entry.getValue();
List<GridPoint> copy = new ArrayList<>(points.size());
points.forEach(p -> copy.add(new GridPoint(p)));
map.setArticulations(entry.getKey(), copy);
}
return map;
}
private String normalizeVertexName(String name) {
if (name.length() > 8) {
return name.substring(0, 8);
}
return name + " ".substring(name.length());
}
private Iterable<GridPoint> allPoints() {
Stream<GridPoint> vPoints = vertexPoints.values().stream();
Stream<GridPoint> ePoints = edgePoints.values().stream().flatMap(l -> l.stream());
Stream<GridPoint> streams = Stream.concat(vPoints, ePoints);
return () -> streams.iterator();
}
public boolean containsPoint(GridPoint p) {
return gridBounds.contains(p);
}
}

View file

@ -0,0 +1,64 @@
/* ###
* 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.viewer.layout;
import java.util.Objects;
/**
* Row and column information for points in a {@link GridLocationMap}. Using these instead
* of java Points, makes the code that translates from grid space to layout space much less
* confusing.
*/
public class GridPoint {
public int row;
public int col;
public GridPoint(int row, int col) {
this.row = row;
this.col = col;
}
public GridPoint(GridPoint point) {
this.row = point.row;
this.col = point.col;
}
@Override
public int hashCode() {
return Objects.hash(col, row);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
GridPoint other = (GridPoint) obj;
return col == other.col && row == other.row;
}
@Override
public String toString() {
return "(r=" + row + ",c=" + col + ")";
}
}

View file

@ -0,0 +1,78 @@
/* ###
* 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.viewer.layout;
import java.util.Objects;
/**
* Class for reporting the min/max columns in a row or the min/max rows in a column
*/
public class GridRange {
public int min;
public int max;
public GridRange() {
this(Integer.MAX_VALUE, Integer.MIN_VALUE);
}
public GridRange(int min, int max) {
this.min = min;
this.max = max;
}
public void add(int value) {
min = Math.min(value, min);
max = Math.max(value, max);
}
@Override
public String toString() {
return "[" + min + " -> " + max + "]";
}
public boolean isEmpty() {
return min > max;
}
public boolean contains(int value) {
return value >= min && value <= max;
}
@Override
public int hashCode() {
return Objects.hash(max, min);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
GridRange other = (GridRange) obj;
return max == other.max && min == other.min;
}
public int width() {
if (isEmpty()) {
return 0;
}
return max - min + 1;
}
}

View file

@ -15,9 +15,9 @@
*/
package ghidra.graph.viewer.layout;
import java.awt.*;
import java.awt.Rectangle;
import java.awt.Shape;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import com.google.common.base.Function;
@ -43,7 +43,7 @@ public class LayoutLocationMap<V, E> {
private int numColumns;
private TreeMap<Integer, Row<V>> rowsByIndex = new TreeMap<>();
private TreeMap<Integer, Column> columnsByIndex = new TreeMap<>();
private TreeMap<Integer, Column<V>> columnsByIndex = new TreeMap<>();
private boolean isCondensed = false;
private GridLocationMap<V, E> gridLocations;
@ -54,29 +54,12 @@ public class LayoutLocationMap<V, E> {
this.gridLocations = gridLocations;
Set<V> vertices = gridLocations.vertices();
Set<E> edges = gridLocations.edges();
MinMaxRowColumn minMax = getMinMaxRowColumnValues(vertices, edges, monitor);
numRows = minMax.maxRow + 1;
numColumns = minMax.maxCol + 1;
numRows = gridLocations.height();
numColumns = gridLocations.width();
initializeLayoutLocations(transformer, vertices, monitor);
}
private LayoutLocationMap() {
// copy constructor
}
public LayoutLocationMap<V, E> copy() {
LayoutLocationMap<V, E> map = new LayoutLocationMap<>();
map.isCondensed = isCondensed;
map.numRows = numRows;
map.numColumns = numColumns;
map.rowsByIndex = new TreeMap<>(rowsByIndex);
map.columnsByIndex = new TreeMap<>(columnsByIndex);
map.gridLocations = gridLocations.copy();
return map;
}
public void dispose() {
rowsByIndex.clear();
columnsByIndex.clear();
@ -90,19 +73,19 @@ public class LayoutLocationMap<V, E> {
return numColumns;
}
public Column col(V v) {
public Column<V> col(V v) {
Integer col = gridLocations.col(v);
return doGetColumn(col);
}
public Column col(int gridX) {
public Column<V> col(int gridX) {
return doGetColumn(gridX);
}
public Column getColumnContaining(int x) {
Column column = null;
Collection<Column> values = columnsByIndex.values();
for (Column nextColumn : values) {
public Column<V> getColumnContaining(int x) {
Column<V> column = null;
Collection<Column<V>> values = columnsByIndex.values();
for (Column<V> nextColumn : values) {
if (x < nextColumn.x) {
return column;
}
@ -111,10 +94,10 @@ public class LayoutLocationMap<V, E> {
return column;
}
private Column doGetColumn(int index) {
Column column = columnsByIndex.get(index);
private Column<V> doGetColumn(int index) {
Column<V> column = columnsByIndex.get(index);
if (column == null) {
column = new Column(index);
column = new Column<>(index);
columnsByIndex.put(index, column);
}
return column;
@ -125,10 +108,10 @@ public class LayoutLocationMap<V, E> {
*
* @return the columns in this location map, sorted from lowest index to highest
*/
public Collection<Column> columns() {
List<Column> result = new ArrayList<>();
Collection<Column> values = columnsByIndex.values();
for (Column column : values) {
public Collection<Column<V>> columns() {
List<Column<V>> result = new ArrayList<>();
Collection<Column<V>> values = columnsByIndex.values();
for (Column<V> column : values) {
result.add(column);
}
return result;
@ -148,17 +131,17 @@ public class LayoutLocationMap<V, E> {
return results;
}
public Column lastColumn() {
public Column<V> lastColumn() {
Entry<Integer, Column> lastEntry = columnsByIndex.lastEntry();
Entry<Integer, Column<V>> lastEntry = columnsByIndex.lastEntry();
if (lastEntry == null) {
return null;
}
return lastEntry.getValue();
}
public Column nextColumn(Column column) {
Column nextColumn = doGetColumn(column.index + 1);
public Column<V> nextColumn(Column<V> column) {
Column<V> nextColumn = doGetColumn(column.index + 1);
if (!nextColumn.isInitialized()) {
// last column?
nextColumn.x = column.x + column.getPaddedWidth(isCondensed);
@ -166,12 +149,12 @@ public class LayoutLocationMap<V, E> {
return nextColumn;
}
public List<Point> articulations(E e) {
public List<GridPoint> articulations(E e) {
return gridLocations.getArticulations(e);
}
public Row<V> row(V v) {
Integer row = gridLocations.row(v);
int row = gridLocations.row(v);
return doGetRow(row);
}
@ -215,7 +198,7 @@ public class LayoutLocationMap<V, E> {
public List<Integer> getColOffsets() {
ArrayList<Integer> list = new ArrayList<>();
for (Column column : columnsByIndex.values()) {
for (Column<V> column : columnsByIndex.values()) {
list.add(column.x);
}
return list;
@ -231,70 +214,55 @@ public class LayoutLocationMap<V, E> {
columnsByIndex + "]";
}
public GridCoordinates getGridCoordinates() {
Row<?> lastRow = lastRow();
Column<?> lastColumn = lastColumn();
if (lastRow == null || lastColumn == null) {
return new GridCoordinates(new int[0], new int[0]);
}
// add 1 to compute a row y value and a column x value for closing the grid
int[] rowStarts = new int[lastRow.index + 1];
int[] colStarts = new int[lastColumn.index + 1];
for (Row<?> row : rowsByIndex.values()) {
rowStarts[row.index] = row.y;
}
for (Column<?> col : columnsByIndex.values()) {
colStarts[col.index] = col.x;
}
// Give any empty rows or columns the coordinate of the row or column that precedes it
// since it takes no space. (Otherwise all the empty row or column labels would overwrite
// themselves at the 0 row or 0 column.
for (int row = 1; row < rowStarts.length; row++) {
if (rowStarts[row] == 0) {
rowStarts[row] = rowStarts[row - 1];
}
}
for (int col = 1; col < colStarts.length; col++) {
if (colStarts[col] == 0) {
colStarts[col] = colStarts[col - 1];
}
}
// close the grid
rowStarts[rowStarts.length - 1] = lastRow.y + lastRow.getPaddedHeight(isCondensed);
colStarts[colStarts.length - 1] = lastColumn.x + lastColumn.getPaddedWidth(isCondensed);
return new GridCoordinates(rowStarts, colStarts);
}
//==================================================================================================
// Initialization Code
//==================================================================================================
private MinMaxRowColumn getMinMaxRowColumnValues(Collection<V> vertices, Collection<E> edges,
TaskMonitor monitor) throws CancelledException {
MinMaxRowColumn minMax = new MinMaxRowColumn();
for (V v : vertices) {
monitor.checkCancelled();
int row = gridLocations.row(v);
if (row > minMax.maxRow) {
minMax.maxRow = row;
}
if (row < minMax.minRow) {
minMax.minRow = row;
}
int column = gridLocations.col(v);
if (column > minMax.maxCol) {
minMax.maxCol = column;
}
if (column < minMax.minCol) {
minMax.minCol = column;
}
}
for (E edge : edges) {
monitor.checkCancelled();
List<Point> articulations = gridLocations.getArticulations(edge);
if (articulations.isEmpty()) {
continue;
}
for (Point location : articulations) {
int row = location.y;
if (row > minMax.maxRow) {
minMax.maxRow = row;
}
if (row < minMax.minRow) {
minMax.minRow = row;
}
int column = location.x;
if (column > minMax.maxCol) {
minMax.maxCol = column;
}
if (column < minMax.minCol) {
minMax.minCol = column;
}
}
}
return minMax;
}
private void initializeLayoutLocations(Function<V, Shape> transformer, Collection<V> vertices,
TaskMonitor monitor) throws CancelledException {
// create this class's rows from the grid
List<Row<V>> gridRows = gridLocations.rows();
Collection<Row<V>> gridRows = gridLocations.rowsMap().values();
for (Row<V> row : gridRows) {
rowsByIndex.put(row.index, row);
}
@ -308,7 +276,7 @@ public class LayoutLocationMap<V, E> {
monitor.checkCancelled();
Row<V> row = row(vertex);
Column column = col(vertex);
Column<V> column = col(vertex);
Shape shape = transformer.apply(vertex);
Rectangle bounds = shape.getBounds();
if (bounds.width > column.width) {
@ -349,7 +317,7 @@ public class LayoutLocationMap<V, E> {
for (int i = 0; i < n; i++) {
monitor.checkCancelled();
Column column = col(i);
Column<V> column = col(i);
column.x = offset;
offset += column.getPaddedWidth(isCondensed);
}

View file

@ -1,23 +0,0 @@
/* ###
* 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.viewer.layout;
public class MinMaxRowColumn {
public int minRow = Integer.MAX_VALUE;
public int maxRow = -1;
public int minCol = Integer.MAX_VALUE;
public int maxCol = -1;
}

View file

@ -29,7 +29,7 @@ import ghidra.graph.viewer.GraphViewerUtils;
* of a grid.
*
* <p>This class maintains a collection of vertices on this row, organized by column index. You
* can get the column of a vertex from {@link #getColumn(Object) getColumn(V)}.
* can get the column of a vertex from {@link #getColumn(Object)}
*
* @param <V> the vertex type
*/
@ -42,7 +42,7 @@ public class Row<V> {
/** The grid index of this row (0, 1...n) for the number of rows */
public int index = Integer.MAX_VALUE;
// Note: this must change together (they are effectively a BiDi map)
// Note: these must change together (they are effectively a BiDi map)
private TreeMap<Integer, V> verticesByColumn = new TreeMap<>();
private Map<V, Integer> columnsByVertex = new HashMap<>();

View file

@ -0,0 +1,101 @@
/* ###
* 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.viewer.renderer;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.visualization.*;
import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.graph.viewer.layout.GridCoordinates;
/**
* Class for painting the underlying grid used to layout a graph. Used as a visual aid when
* debugging grid based graph layouts.
*/
public class GridPainter {
private GridCoordinates grid;
public GridPainter(GridCoordinates gridCoordinates) {
this.grid = gridCoordinates;
}
public void paintLayoutGridCells(RenderContext<?, ?> renderContext, Layout<?, ?> layout) {
if (grid == null) {
return;
}
int rowCount = grid.rowCount();
int colCount = grid.columnCount();
GraphicsDecorator g = renderContext.getGraphicsContext();
Color originalColor = g.getColor();
Color gridColor = Palette.ORANGE;
Color textColor = Palette.BLACK;
Rectangle bounds = grid.getBounds();
int width = bounds.width;
int height = bounds.height;
MultiLayerTransformer transformer = renderContext.getMultiLayerTransformer();
int previous = -1;
for (int row = 0; row < rowCount; row++) {
int y = grid.y(row);
if (y == previous) {
continue; // don't paint empty rows
}
previous = y;
Point2D start = new Point2D.Double(0, y);
Point2D end = new Point2D.Double(width, y);
start = transformer.transform(Layer.LAYOUT, start);
end = transformer.transform(Layer.LAYOUT, end);
g.setColor(textColor);
g.drawString(Integer.toString(row), (float) start.getX() - 20,
(float) (start.getY() + 5));
g.setColor(gridColor);
g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY());
}
previous = -1;
for (int col = 0; col < colCount; col++) {
int x = grid.x(col);
if (x == previous) {
continue;
}
previous = x;
Point2D start = new Point2D.Double(x, 0);
Point2D end = new Point2D.Double(x, height);
start = transformer.transform(Layer.LAYOUT, start);
end = transformer.transform(Layer.LAYOUT, end);
g.setColor(textColor);
g.drawString(Integer.toString(col), (float) start.getX() - 5,
(float) (start.getY() - 10));
g.setColor(gridColor);
g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY());
}
g.setColor(originalColor);
}
}

View file

@ -15,21 +15,15 @@
*/
package ghidra.graph.viewer.renderer;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.util.*;
import com.google.common.base.Function;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.visualization.*;
import edu.uci.ics.jung.visualization.layout.ObservableCachingLayout;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.renderers.Renderer;
import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.graph.viewer.*;
import ghidra.graph.viewer.edge.BasicEdgeLabelRenderer;
import ghidra.graph.viewer.layout.*;
/**
* This was created to add the ability to paint selected vertices above other vertices. We need
@ -42,11 +36,16 @@ import ghidra.graph.viewer.layout.*;
public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>>
extends edu.uci.ics.jung.visualization.renderers.BasicRenderer<V, E> {
private static GridPainter gridPainter;
/**
* Used for displaying grid information for graph layouts
* Sets a painter to show an underlying grid. (To see a layout's associated grid, search
* for calls to this method and un-comment them)
* @param gridPainter A painter that paints the grid that a layout was based on.
*/
public static Map<VisualGraphLayout<?, ?>, LayoutLocationMap<?, ?>> DEBUG_ROW_COL_MAP =
new HashMap<>();
public static void setGridPainter(GridPainter gridPainter) {
VisualGraphRenderer.gridPainter = gridPainter;
}
private Renderer.EdgeLabel<V, E> edgeLabelRenderer = new BasicEdgeLabelRenderer<>();
@ -73,6 +72,9 @@ public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>
private void mimickSuperPaintingWithoutPaintingSelectedVertices(
RenderContext<V, E> renderContext, Layout<V, E> layout) {
if (gridPainter != null) {
gridPainter.paintLayoutGridCells(renderContext, layout);
}
for (E e : layout.getGraph().getEdges()) {
renderEdge(renderContext, layout, e);
@ -94,7 +96,6 @@ public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>
// renderEdgeLabel(renderContext, layout, e);
// }
paintLayoutGridCells(renderContext, layout);
}
@Override
@ -124,81 +125,4 @@ public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>
edgeLabelRenderer.labelEdge(rc, layout, e, xform.apply(e));
}
@SuppressWarnings({ "unchecked", "rawtypes" }) // the types in the cast matter not
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'
Layout<V, E> key = layout;
if (layout instanceof ObservableCachingLayout) {
key = ((ObservableCachingLayout) layout).getDelegate();
}
LayoutLocationMap<?, ?> locationMap = DEBUG_ROW_COL_MAP.get(key);
if (locationMap == null) {
return;
}
int rowCount = locationMap.getRowCount();
if (rowCount == 0) {
return; // ?
}
GraphicsDecorator g = renderContext.getGraphicsContext();
Color originalColor = g.getColor();
Color gridColor = Palette.ORANGE;
Color textColor = Palette.BLACK;
boolean isCondensed = locationMap.isCondensed();
Row<?> lastRow = locationMap.lastRow();
Column lastColumn = locationMap.lastColumn();
if (lastRow == null || lastColumn == null) {
return; // empty graph?
}
int width = lastColumn.x + lastColumn.getPaddedWidth(isCondensed);
int height = lastRow.y + lastRow.getPaddedHeight(isCondensed);
MultiLayerTransformer transformer = renderContext.getMultiLayerTransformer();
for (Row<?> row : locationMap.rows()) {
Point2D start = new Point2D.Double(0, row.y);
start = transformer.transform(Layer.LAYOUT, start);
g.setColor(textColor);
g.drawString(Integer.toString(row.index), (float) start.getX() - 20,
(float) (start.getY() + 5));
Point2D end = new Point2D.Double(width, row.y);
end = transformer.transform(Layer.LAYOUT, end);
g.setColor(gridColor);
g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY());
}
// close the grid
Point2D start = new Point2D.Double(0, lastRow.y + lastRow.getPaddedHeight(isCondensed));
start = transformer.transform(Layer.LAYOUT, start);
Point2D end = new Point2D.Double(width, lastRow.y + lastRow.getPaddedHeight(isCondensed));
end = transformer.transform(Layer.LAYOUT, end);
g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY());
for (Column column : locationMap.columns()) {
start = new Point2D.Double(column.x, 0);
start = transformer.transform(Layer.LAYOUT, start);
g.setColor(textColor);
g.drawString(Integer.toString(column.index), (float) start.getX() - 5,
(float) (start.getY() - 10));
end = new Point2D.Double(column.x, height);
end = transformer.transform(Layer.LAYOUT, end);
g.setColor(gridColor);
g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY());
}
// close the grid
start = new Point2D.Double(lastColumn.x + lastColumn.getPaddedWidth(isCondensed), 0);
start = transformer.transform(Layer.LAYOUT, start);
end = new Point2D.Double(lastColumn.x + lastColumn.getPaddedWidth(isCondensed), height);
end = transformer.transform(Layer.LAYOUT, end);
g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY());
g.setColor(originalColor);
}
}

View file

@ -34,10 +34,12 @@ import docking.test.AbstractDockingTest;
import edu.uci.ics.jung.algorithms.layout.Layout;
import ghidra.graph.graphs.AbstractTestVertex;
import ghidra.graph.graphs.TestEdge;
import ghidra.graph.support.*;
import ghidra.graph.support.TestLayoutProvider;
import ghidra.graph.support.TestVisualGraph;
import ghidra.graph.viewer.event.mouse.VisualGraphMouseTrackingGraphMousePlugin;
import ghidra.graph.viewer.event.mouse.VisualGraphPluggableGraphMouse;
import ghidra.graph.viewer.event.picking.GPickedState;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -73,7 +75,6 @@ public abstract class AbstractVisualGraphTest extends AbstractDockingTest {
protected void buildAndLayoutGraph() throws CancelledException {
// the test machine has odd Swing exceptions when we construct UIs off the Swing thread
graph = runSwing(() -> buildGraph());
TestLayoutProvider layoutProvider = createLayoutProvider();
graph.setLayout(layoutProvider.getLayout(graph, TaskMonitor.DUMMY));
graphComponent = runSwing(() -> createGraphComponent(layoutProvider));
@ -190,7 +191,7 @@ public abstract class AbstractVisualGraphTest extends AbstractDockingTest {
GraphViewerUtils.translatePointFromViewSpaceToLayoutSpace(viewPoint, viewer);
swing(() -> {
TestGraphLayout layout = graph.getLayout();
VisualGraphLayout<AbstractTestVertex, TestEdge> layout = graph.getLayout();
Point2D p = layout.apply(v);
layout.setLocation(v,
new Point2D.Double(p.getX() + layoutPoint.getX(), p.getY() + layoutPoint.getY()));

View file

@ -26,12 +26,11 @@ import java.util.List;
import org.junit.Test;
import edu.uci.ics.jung.algorithms.layout.KKLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import generic.test.AbstractGTest;
import ghidra.graph.graphs.*;
import ghidra.graph.support.*;
import ghidra.graph.support.TestVertexTooltipProvider;
import ghidra.graph.support.TestVertexTooltipProvider.SpyTooltip;
import ghidra.graph.support.TestVisualGraph;
public class GraphViewerTest extends AbstractVisualGraphTest {
@ -53,16 +52,6 @@ public class GraphViewerTest extends AbstractVisualGraphTest {
return g;
}
@Override
protected TestLayoutProvider createLayoutProvider() {
return new TestLayoutProvider() {
@Override
protected Layout<AbstractTestVertex, TestEdge> createJungLayout(TestVisualGraph g) {
return new KKLayout<>(g);
}
};
}
@Override
protected void initialize() {
viewer = graphComponent.getPrimaryViewer();

View file

@ -77,10 +77,10 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
protected TestV[] generateSimplyConnectedGraph(int nVertices) {
TestV[] vertices = new TestV[nVertices];
for (int i = 0; i < nVertices; i++) {
vertices[i] = vertex(i);
vertices[i] = v(i);
}
for (int i = 0; i < nVertices - 1; i++) {
edge(vertices[i], vertices[i + 1]);
e(vertices[i], vertices[i + 1]);
}
return vertices;
}
@ -88,12 +88,12 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
protected TestV[] generateCompletelyConnectedGraph(int nVertices) {
TestV[] vertices = new TestV[nVertices];
for (int i = 0; i < nVertices; i++) {
vertices[i] = vertex(i);
vertices[i] = v(i);
}
for (int i = 0; i < nVertices; i++) {
for (int j = 0; j < nVertices; j++) {
if (i != j) {
edge(vertices[i], vertices[j]);
e(vertices[i], vertices[j]);
}
}
}
@ -103,12 +103,12 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
protected TestV[] generateHalflyConnectedGraph(int nVertices) {
TestV[] vertices = new TestV[nVertices];
for (int i = 0; i < nVertices; i++) {
vertices[i] = vertex(i);
vertices[i] = v(i);
}
// at least one straight line through the graph
for (int i = 0; i < nVertices - 1; i++) {
edge(vertices[i], vertices[i + 1]);
e(vertices[i], vertices[i + 1]);
}
// extra connections
@ -116,7 +116,7 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
for (int i = 0; i < nVertices; i++) {
for (int j = 0; j < n; j++) {
if (i != j) {
edge(vertices[i], vertices[j]);
e(vertices[i], vertices[j]);
}
}
}
@ -126,12 +126,12 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
protected TestV[] generateHalflyConnectedGraphNoBacktracking(int nVertices) {
TestV[] vertices = new TestV[nVertices];
for (int i = 0; i < nVertices; i++) {
vertices[i] = vertex(i);
vertices[i] = v(i);
}
// at least one straight line through the graph
for (int i = 0; i < nVertices - 1; i++) {
edge(vertices[i], vertices[i + 1]);
e(vertices[i], vertices[i + 1]);
}
// extra connections
@ -139,7 +139,7 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
for (int i = 0; i < nVertices; i++) {
for (int j = i; j < n; j++) {
if (i != j) {
edge(vertices[i], vertices[j]);
e(vertices[i], vertices[j]);
}
}
}
@ -165,15 +165,15 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest {
Assert.fail("Unexpected set size");
}
protected TestV vertex(int id) {
protected TestV v(int id) {
return new TestV(id);
}
protected TestV vertex(String id) {
protected TestV v(String id) {
return new TestV(id);
}
protected TestE edge(TestV start, TestV end) {
protected TestE e(TestV start, TestV end) {
TestE e = new TestE(start, end);
g.addEdge(e);
return e;

View file

@ -88,34 +88,34 @@ public class GraphAlgorithmsVisualDebugger extends AbstractGraphAlgorithmsTest {
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);
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
TestV v7 = v(7);
TestV v8 = v(8);
TestV v9 = v(9);
TestV v10 = v(10);
edge(v1, v2);
edge(v1, v3);
e(v1, v2);
e(v1, v3);
edge(v2, v1); // back edge
e(v2, v1); // back edge
edge(v3, v4);
edge(v3, v5);
edge(v3, v6);
e(v3, v4);
e(v3, v5);
e(v3, v6);
edge(v5, v10);
e(v5, v10);
edge(v6, v7);
e(v6, v7);
edge(v7, v8);
edge(v7, v9);
e(v7, v8);
e(v7, v9);
edge(v9, v6); // back edge
e(v9, v6); // back edge
AlgorithmSteppingTaskMonitor steppingMonitor = new AlgorithmSteppingTaskMonitor();
steppingMonitor = new AlgorithmSelfSteppingTaskMonitor(500);

View file

@ -0,0 +1,242 @@
/* ###
* 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.*;
import java.util.*;
import org.junit.Test;
public class GraphToTreeTest extends AbstractGraphAlgorithmsTest {
private Comparator<TestE> edgeComparator = (e1, e2) -> e1.toString().compareTo(e2.toString());
@Override
protected GDirectedGraph<TestV, TestE> createGraph() {
return GraphFactory.createDirectedGraph();
}
@Test
public void testSimpleGraph() {
// v1 -> v2 -> v3
// |
// v4 -> v5 -> v6
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
e(v1, v2);
e(v2, v3);
e(v1, v4);
e(v4, v5);
e(v5, v6);
// expected same graph
// v1 -> v2 -> v3
// |
// v4 -> v5 -> v6
GDirectedGraph<TestV, TestE> tree = GraphAlgorithms.toTree(g, v1, edgeComparator);
assertEdges(tree, e(v1, v2), e(v2, v3), e(v1, v4), e(v4, v5), e(v5, v6));
}
@Test
public void testGraphWithLoop() {
// v1 -> v2 -> v3 <----
// | |
// v4 -> v5 -> v6 |
// | |
// |->---->----->-----|
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
e(v1, v2);
e(v2, v3);
e(v1, v4);
e(v4, v5);
e(v5, v6);
e(v4, v3);
// expected:
// v1 -> v2 -> v3
// |
// v4 -> v5 -> v6
GDirectedGraph<TestV, TestE> tree = GraphAlgorithms.toTree(g, v1, edgeComparator);
assertEdges(tree, e(v1, v2), e(v2, v3), e(v1, v4), e(v4, v5), e(v5, v6));
}
@Test
public void testDoubleLoop() {
// V1 <- V4 <--------
// | ^ ^
// v | |
// V2 -> V3 -> V5 -> V6
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
e(v1, v2);
e(v2, v3);
e(v3, v4);
e(v3, v5);
e(v5, v6);
e(v6, v4);
e(v4, v1);
// expected:
//
// V1 -> V2 -> V3 -> V5 -> V6 -> V4
GDirectedGraph<TestV, TestE> tree = GraphAlgorithms.toTree(g, v1, edgeComparator);
assertEdges(tree, e(v1, v2), e(v2, v3), e(v3, v5), e(v5, v6), e(v6, v4));
}
@Test
public void testInterleavedBranching() {
/*
.<-v1->.
| | |
| v4 |
| | |
>--v3 |
| |
v2--<
*/
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
e(v1, v3);
e(v1, v4);
e(v1, v2);
e(v4, v3);
e(v3, v2);
// expected:
// v1-> v4 -> v3 -> v2
GDirectedGraph<TestV, TestE> tree = GraphAlgorithms.toTree(g, v1, edgeComparator);
assertEdges(tree, e(v1, v4), e(v4, v3), e(v3, v2));
}
@Test
public void testMixedGraph() {
//@formatter:off
/*
v1
|
v2
|
sink v3 <- v4
/\ |
| v5
| / \
| v6 v7
| |\ |
| v8 \ |
| \ |
| \ |
| v9
| / \
| / \
.<--v11-->v10 sink
*/
//@formatter:on
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
TestV v7 = v(7);
TestV v8 = v(8);
TestV v9 = v(9);
TestV v10 = v(10);
TestV v11 = v(11);
e(v1, v2);
e(v2, v4);
e(v4, v3);
e(v4, v5);
e(v5, v6);
e(v5, v7);
e(v6, v8);
e(v6, v9);
e(v7, v9);
e(v8, v9);
e(v9, v10);
e(v9, v11);
e(v11, v3);
e(v11, v10);
//@formatter:off
/* expected
v1
|
v2
|
sink v4
|
v5
/ \
v6 v7
|
v8
|
v9
|
v11-->v10
|
v3
*/
//@formatter:on
GDirectedGraph<TestV, TestE> tree = GraphAlgorithms.toTree(g, v1, edgeComparator);
assertEdges(tree, e(v1, v2), e(v2, v4), e(v4, v5), e(v5, v6),
e(v5, v7), e(v6, v8), e(v8, v9), e(v9, v11), e(v11, v10), e(v11, v3));
}
private void assertEdges(GDirectedGraph<TestV, TestE> tree, TestE... edges) {
Set<TestE> allEdges = new HashSet<>(tree.getEdges());
assertEquals("edge count: ", edges.length, allEdges.size());
for (TestE edge : edges) {
if (!allEdges.contains(edge)) {
fail("Missing expected edge: " + edge);
}
}
}
}

View file

@ -58,25 +58,25 @@ public class MutableGDirectedGraphWrapperTest extends AbstractGraphAlgorithmsTes
v6
*/
TestV v1 = vertex(1); // Root
TestV v2 = vertex(2); // Root
TestV v3 = vertex(3);
TestV v4 = vertex(4);
TestV v5 = vertex(5);
TestV v6 = vertex(6);
TestV v1 = v(1); // Root
TestV v2 = v(2); // Root
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
TestE e1 = edge(v1, v3);
TestE e2 = edge(v3, v5);
TestE e3 = edge(v5, v6);
TestE e1 = e(v1, v3);
TestE e2 = e(v3, v5);
TestE e3 = e(v5, v6);
TestE e4 = edge(v2, v4);
TestE e5 = edge(v4, v6);
TestE e4 = e(v2, v4);
TestE e5 = e(v4, v6);
//
// Now create the second graph above using a mutable wrapper
//
MutableGDirectedGraphWrapper<TestV, TestE> wrapper = new MutableGDirectedGraphWrapper<>(g);
TestV fakeRoot = vertex("Fake Root");
TestV fakeRoot = v("Fake Root");
TestE fakeEdge1 = new TestE(fakeRoot, v1);
TestE fakeEdge2 = new TestE(fakeRoot, v2);

View file

@ -0,0 +1,212 @@
/* ###
* 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.*;
import java.util.*;
import java.util.stream.Collectors;
import org.junit.Test;
public class TopologicalGraphSortTest extends AbstractGraphAlgorithmsTest {
private Comparator<TestE> edgeComparator = (e1, e2) -> e1.toString().compareTo(e2.toString());
@Override
protected GDirectedGraph<TestV, TestE> createGraph() {
return GraphFactory.createDirectedGraph();
}
@Test
public void testSimpleGraph() {
// v1 -> v2 -> v3
// |
// v4 -> v5 -> v6
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
e(v1, v2);
e(v2, v3);
e(v1, v4);
e(v4, v5);
e(v5, v6);
List<TestV> sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator);
assertList(sorted, v1, v2, v3, v4, v5, v6);
}
@Test
public void testGraphWithLoop() {
// v1 -> v2 -> v3 <----
// | |
// v4 -> v5 -> v6 |
// | |
// |->---->----->-----|
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
e(v1, v2);
e(v2, v3);
e(v1, v4);
e(v4, v5);
e(v5, v6);
e(v4, v3);
List<TestV> sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator);
assertList(sorted, v1, v2, v4, v3, v5, v6);
}
@Test
public void testDoubleLoop() {
// V1 <- V4 <--------
// | ^ ^
// v | |
// V2 -> V3 -> V5 -> V6
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
e(v1, v2);
e(v2, v3);
e(v3, v4);
e(v3, v5);
e(v5, v6);
e(v6, v4);
e(v4, v1);
List<TestV> sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator);
assertList(sorted, v1, v2, v3, v5, v6, v4);
}
@Test
public void testInterleavedBranching() {
/*
.<-v1->.
| | |
| v4 |
| | |
>--v3 |
| |
v2--<
*/
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
e(v1, v3);
e(v1, v4);
e(v1, v2);
e(v4, v3);
e(v3, v2);
List<TestV> sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator);
assertList(sorted, v1, v4, v3, v2);
}
@Test
public void testMixedGraph() {
//@formatter:off
/*
v1
|
v2
|
sink v3 <- v4
/\ |
| v5
| / \
| v6 v7
| |\ |
| v8 \ |
| \ |
| \ |
| v9
| / \
| / \
.<--v11-->v10 sink
*/
//@formatter:on
TestV v1 = v(1);
TestV v2 = v(2);
TestV v3 = v(3);
TestV v4 = v(4);
TestV v5 = v(5);
TestV v6 = v(6);
TestV v7 = v(7);
TestV v8 = v(8);
TestV v9 = v(9);
TestV v10 = v(10);
TestV v11 = v(11);
e(v1, v2);
e(v2, v4);
e(v4, v3);
e(v4, v5);
e(v5, v6);
e(v5, v7);
e(v6, v8);
e(v6, v9);
e(v7, v9);
e(v8, v9);
e(v9, v10);
e(v9, v11);
e(v11, v3);
e(v11, v10);
List<TestV> sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator);
assertList(sorted, v1, v2, v4, v5, v6, v8, v7, v9, v11, v10, v3);
}
private void assertList(List<TestV> sorted, TestV... vertices) {
if (vertices.length != sorted.size()) {
failed(sorted, vertices);
}
assertEquals(vertices.length, sorted.size());
for (int i = 0; i < vertices.length; i++) {
if (vertices[i] != sorted.get(i)) {
failed(sorted, vertices);
}
}
}
private void failed(List<TestV> sorted, TestV[] vertices) {
String expected =
Arrays.stream(vertices).map(v -> v.toString()).collect(Collectors.joining(", "));
String actual = sorted.stream().map(v -> v.toString()).collect(Collectors.joining(", "));
fail("expected: \"" + expected + "\", but got: \"" + actual + "\"");
}
}

View file

@ -314,13 +314,10 @@ public class TestGraphAlgorithmSteppingViewerPanel<V, E extends GEdge<V>> extend
GridLocationMap<AlgorithmTestSteppingVertex<V>, AlgorithmTestSteppingEdge<V>> grid,
int row, int col) {
int existing = grid.row(v);
if (existing > 0) {
return; // already processed
if (grid.containsVertex(v)) {
return;
}
grid.row(v, row);
grid.col(v, col);
grid.set(v, row, col);
int nextRow = row++;
Collection<AlgorithmTestSteppingEdge<V>> children = g.getOutEdges(v);

View file

@ -27,20 +27,23 @@ import generic.theme.GThemeDefaults.Colors.Palette;
*/
public class LabelTestVertex extends AbstractTestVertex {
private JLabel label = new GDLabel();
private JLabel label;
public LabelTestVertex(String name) {
super(name);
label.setText(name);
}
@Override
public JComponent getComponent() {
if (label == null) {
label = new GDLabel();
label.setText(getName());
label.setPreferredSize(new Dimension(50, 50));
label.setBackground(Palette.GOLD);
label.setOpaque(true);
label.setBorder(BorderFactory.createRaisedBevelBorder());
label.setHorizontalAlignment(SwingConstants.CENTER);
}
@Override
public JComponent getComponent() {
return label;
}

View file

@ -22,7 +22,7 @@ import javax.swing.JComponent;
*/
public class TestVertex extends AbstractTestVertex {
protected TestVertex(String name) {
public TestVertex(String name) {
super(name);
}

View file

@ -22,9 +22,10 @@ import javax.swing.Icon;
import edu.uci.ics.jung.algorithms.layout.DAGLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import ghidra.graph.graphs.TestEdge;
import ghidra.graph.graphs.AbstractTestVertex;
import ghidra.graph.graphs.TestEdge;
import ghidra.graph.viewer.layout.LayoutProvider;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.graph.viewer.renderer.ArticulatedEdgeRenderer;
import ghidra.graph.viewer.shape.ArticulatedEdgeTransformer;
import ghidra.util.exception.CancelledException;
@ -33,7 +34,8 @@ import ghidra.util.task.TaskMonitor;
/**
* A layout provider used for testing.
*/
public class TestLayoutProvider implements LayoutProvider<AbstractTestVertex, TestEdge, TestVisualGraph> {
public class TestLayoutProvider
implements LayoutProvider<AbstractTestVertex, TestEdge, TestVisualGraph> {
private ArticulatedEdgeTransformer<AbstractTestVertex, TestEdge> edgeShapeTransformer =
new ArticulatedEdgeTransformer<>();
@ -41,7 +43,8 @@ public class TestLayoutProvider implements LayoutProvider<AbstractTestVertex, Te
new ArticulatedEdgeRenderer<>();
@Override
public TestGraphLayout getLayout(TestVisualGraph g, TaskMonitor monitor)
public VisualGraphLayout<AbstractTestVertex, TestEdge> getLayout(TestVisualGraph g,
TaskMonitor monitor)
throws CancelledException {
Layout<AbstractTestVertex, TestEdge> jungLayout = new DAGLayout<>(g);
@ -54,11 +57,6 @@ public class TestLayoutProvider implements LayoutProvider<AbstractTestVertex, Te
return new TestGraphLayout(jungLayout);
}
// template method to allow tests to override the Jung layout in use
protected Layout<AbstractTestVertex, TestEdge> createJungLayout(TestVisualGraph g) {
return new DAGLayout<>(g);
}
@Override
public String getLayoutName() {
return "Test Layout";

View file

@ -18,20 +18,21 @@ package ghidra.graph.support;
import java.util.Collection;
import ghidra.graph.graphs.*;
import ghidra.graph.viewer.layout.VisualGraphLayout;
/**
* A visual graph implementation used for testing.
*/
public class TestVisualGraph extends DefaultVisualGraph<AbstractTestVertex, TestEdge> {
private TestGraphLayout layout;
private VisualGraphLayout<AbstractTestVertex, TestEdge> layout;
@Override
public TestGraphLayout getLayout() {
public VisualGraphLayout<AbstractTestVertex, TestEdge> getLayout() {
return layout;
}
public void setLayout(TestGraphLayout layout) {
public void setLayout(VisualGraphLayout<AbstractTestVertex, TestEdge> layout) {
this.layout = layout;
}

View file

@ -15,8 +15,7 @@
*/
package ghidra.graph.viewer.layout;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
import java.util.List;
@ -244,6 +243,56 @@ public class GridLocationMapTest {
assertCoordinates(v7, 2, 3);
}
@Test
public void testGetVertexColumnRanges() {
V v1 = new V("v1");
V v2 = new V("v2");
V v3 = new V("v3");
locations.set(v1, 0, 0);
locations.set(v2, 2, 4);
locations.set(v3, 2, 6);
GridRange[] columnRanges = locations.getVertexColumnRanges();
assertEquals(3, columnRanges.length);
assertEquals(new GridRange(0, 0), columnRanges[0]);
assertEquals(new GridRange(), columnRanges[1]);
assertEquals(new GridRange(4, 6), columnRanges[2]);
}
@Test
public void testAddGrids() {
// This method creates two grids with points and edge articulations. It then merges
// the other map into the first map with a given row and column shift. It then checks
// the merged grid has all the edge points and vertex points in the expected locations.
V v1 = new V("v1");
V v2 = new V("v2");
E e1 = new E(v1, v2);
V v3 = new V("v3");
V v4 = new V("v4");
E e2 = new E(v3, v4);
locations.set(v1, 0, 0);
locations.set(v2, 2, 4);
locations.setArticulations(e1, List.of(new GridPoint(3, 3)));
GridLocationMap<V, E> otherMap = new GridLocationMap<>();
otherMap.set(v3, 1, 1);
otherMap.set(v4, 2, 2);
otherMap.setArticulations(e1, List.of(new GridPoint(1, 2)));
locations.add(otherMap, 10, 10);
assertEquals(13, locations.width());
assertEquals(13, locations.height());
assertCoordinates(v1, 0, 0);
assertCoordinates(v2, 2, 4);
assertCoordinates(v3, 11, 11);
assertCoordinates(v4, 12, 12);
}
//==================================================================================================
// Private Methods
//==================================================================================================
@ -253,7 +302,7 @@ public class GridLocationMapTest {
List<Row<V>> rows = locations.rows();
Row<V> row = getRow(rows, rowIndex);
assertEquals("Row " + rowIndex + " has wrong column count", size,
(int) row.getColumnCount());
row.getColumnCount());
assertEquals(startColumnIndex, (int) row.getStartColumn());
}
@ -279,8 +328,8 @@ public class GridLocationMapTest {
}
private void assertCoordinates(V v, int row, int col) {
assertEquals("Row not set for '" + v + "'", row, (int) locations.row(v));
assertEquals("Column not set for '" + v + "'", col, (int) locations.col(v));
assertEquals("Row not set for '" + v + "'", row, locations.row(v));
assertEquals("Column not set for '" + v + "'", col, locations.col(v));
}
private class V extends TestVertex {