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

@ -4,9 +4,9 @@
* 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.
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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,19 +214,21 @@ public class FunctionGraphFactory {
"\" (try another layout)";
}
private static boolean performSwingThreadRequiredWork(FunctionGraph functionGraph) {
final Collection<FGVertex> vertices = functionGraph.getVertices();
try {
SystemUtilities.runSwingNow(() -> {
for (FGVertex v : vertices) {
v.getComponent();
}
});
return true;
}
catch (Exception e) {
return false;
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) {
monitor.increment();
try {
Swing.runNow(v::getComponent);
}
catch (Exception e) {
return false;
}
}
return true;
}
private static boolean isEntry(CodeBlock codeBlock) {

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

@ -4,9 +4,9 @@
* 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.
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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);
}
}