From 0a5a2ce9d891d03b5b07254beb6822a8cb0dc464 Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:11:36 -0500 Subject: [PATCH] GP-4988 Created flow chart function graph layout --- .../FunctionGraph/certification.manifest | 2 + .../data/functiongraph.theme.properties | 2 + .../functiongraph/graph/FunctionGraph.java | 8 +- .../graph/FunctionGraphFactory.java | 34 +- .../layout/ExperimentalLayoutProvider.java | 35 - .../flowchart/AbstractFlowChartLayout.java | 245 +++ .../layout/flowchart/ColSegmentList.java | 238 +++ .../graph/layout/flowchart/ColumnSegment.java | 346 +++++ .../graph/layout/flowchart/EdgeSegment.java | 112 ++ .../layout/flowchart/EdgeSegmentMap.java | 118 ++ .../layout/flowchart/FGFlowChartLayout.java | 98 ++ .../flowchart/FlowChartLayoutProvider.java | 54 + .../LeftAlignedFlowChartLayoutProvider.java | 54 + .../flowchart/OrthogonalEdgeRouter.java | 173 +++ .../layout/flowchart/OrthogonalGridSizer.java | 181 +++ .../OrthogonalGridToLayoutMapper.java | 165 ++ .../graph/layout/flowchart/RowSegment.java | 267 ++++ .../layout/flowchart/RowSegmentList.java | 116 ++ .../layout/jungrapht/JgtNamedLayout.java | 10 +- .../ghidra/graph/viewer/FGViewUpdater.java | 9 +- .../images/function_graph_flowchart.png | Bin 0 -> 972 bytes .../images/function_graph_flowchart_left.png | Bin 0 -> 970 bytes .../graph/layout/TestFGLayoutProvider.java | 10 +- .../AbstractFlowChartLayoutTest.java | 420 +++++ .../layout/flowchart/FlowChartLayoutTest.java | 264 ++++ .../LeftAlignedFlowChartLayoutTest.java | 258 ++++ .../layout/flowchart/EdgeSegmentTest.java | 982 ++++++++++++ .../graph/layout/DecompilerNestedLayout.java | 23 +- .../graph/layout/BowTieLayout.java | 6 +- .../src/main/java/util/CollectionUtils.java | 21 +- .../java/ghidra/graph/GraphAlgorithms.java | 44 + .../ghidra/graph/GraphToTreeAlgorithm.java | 216 +++ .../ghidra/graph/viewer/GraphComponent.java | 6 +- .../graph/viewer/VisualGraphViewUpdater.java | 13 +- .../layout/AbstractVisualGraphLayout.java | 22 +- .../ghidra/graph/viewer/layout/Column.java | 38 +- .../graph/viewer/layout/GridBounds.java | 100 ++ .../graph/viewer/layout/GridCoordinates.java | 79 + .../graph/viewer/layout/GridLocationMap.java | 493 +++--- .../ghidra/graph/viewer/layout/GridPoint.java | 64 + .../ghidra/graph/viewer/layout/GridRange.java | 78 + .../viewer/layout/LayoutLocationMap.java | 172 +-- .../graph/viewer/layout/MinMaxRowColumn.java | 23 - .../java/ghidra/graph/viewer/layout/Row.java | 8 +- .../graph/viewer/renderer/GridPainter.java | 101 ++ .../viewer/renderer/VisualGraphRenderer.java | 104 +- .../graph/viewer/AbstractVisualGraphTest.java | 11 +- .../ghidra/graph/viewer/GraphViewerTest.java | 19 +- .../graph/AbstractGraphAlgorithmsTest.java | 30 +- .../ghidra/graph/GraphAlgorithmsTest.java | 1350 ++++++++--------- .../graph/GraphAlgorithmsVisualDebugger.java | 46 +- .../java/ghidra/graph/GraphToTreeTest.java | 242 +++ .../MutableGDirectedGraphWrapperTest.java | 28 +- .../graph/TopologicalGraphSortTest.java | 212 +++ ...TestGraphAlgorithmSteppingViewerPanel.java | 13 +- .../ghidra/graph/graphs/LabelTestVertex.java | 21 +- .../java/ghidra/graph/graphs/TestVertex.java | 6 +- .../graph/support/TestLayoutProvider.java | 18 +- .../ghidra/graph/support/TestVisualGraph.java | 11 +- .../viewer/layout/GridLocationMapTest.java | 63 +- 60 files changed, 6570 insertions(+), 1312 deletions(-) delete mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColSegmentList.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColumnSegment.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegment.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentMap.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FGFlowChartLayout.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutProvider.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutProvider.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalEdgeRouter.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridSizer.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridToLayoutMapper.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegment.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegmentList.java create mode 100644 Ghidra/Features/FunctionGraph/src/main/resources/images/function_graph_flowchart.png create mode 100644 Ghidra/Features/FunctionGraph/src/main/resources/images/function_graph_flowchart_left.png create mode 100644 Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayoutTest.java create mode 100644 Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java create mode 100644 Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutTest.java create mode 100644 Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphToTreeAlgorithm.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridCoordinates.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridRange.java delete mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/MinMaxRowColumn.java create mode 100644 Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/GridPainter.java create mode 100644 Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphToTreeTest.java create mode 100644 Ghidra/Framework/Graph/src/test/java/ghidra/graph/TopologicalGraphSortTest.java diff --git a/Ghidra/Features/FunctionGraph/certification.manifest b/Ghidra/Features/FunctionGraph/certification.manifest index 9759b3d2ae..4d419bb7c5 100644 --- a/Ghidra/Features/FunctionGraph/certification.manifest +++ b/Ghidra/Features/FunctionGraph/certification.manifest @@ -44,6 +44,8 @@ src/main/resources/images/fgpaths.png||GHIDRA||reviewed||END| src/main/resources/images/fgrevblock.png||GHIDRA||reviewed||END| src/main/resources/images/field.header.png||GHIDRA||reviewed|Custom icon|END| src/main/resources/images/fullscreen_view.png||FAMFAMFAM Icons - CC 2.5||||END| +src/main/resources/images/function_graph_flowchart.png||GHIDRA||||END| +src/main/resources/images/function_graph_flowchart_left.png||GHIDRA||||END| src/main/resources/images/graph_view.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/id.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/paintbrush.png||FAMFAMFAM Icons - CC 2.5||||END| diff --git a/Ghidra/Features/FunctionGraph/data/functiongraph.theme.properties b/Ghidra/Features/FunctionGraph/data/functiongraph.theme.properties index 6f47597327..b7ec20a55f 100644 --- a/Ghidra/Features/FunctionGraph/data/functiongraph.theme.properties +++ b/Ghidra/Features/FunctionGraph/data/functiongraph.theme.properties @@ -21,6 +21,8 @@ color.bg.plugin.functiongraph.paint.icon = color.palette.lightcornflowerblue icon.plugin.functiongraph.layout.experimental = package_development.png +icon.plugin.functiongraph.layout.flowchart = function_graph_flowchart.png +icon.plugin.functiongraph.layout.flowchart.left = function_graph_flowchart_left.png icon.plugin.functiongraph.action.vertex.xrefs = brick_link.png icon.plugin.functiongraph.action.vertex.maximize = fullscreen_view.png icon.plugin.functiongraph.action.vertex.minimize = graph_view.png diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java index e148d08833..c92bff3606 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraph.java @@ -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 { 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 { 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 diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraphFactory.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraphFactory.java index 8147120f83..8da82f3670 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraphFactory.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/FunctionGraphFactory.java @@ -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 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 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) { diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java deleted file mode 100644 index 48cb5f44c6..0000000000 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/ExperimentalLayoutProvider.java +++ /dev/null @@ -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 - } -} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java new file mode 100644 index 0000000000..176fc6fcf5 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayout.java @@ -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. + *

+ * 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. + *

+ * Once the vertices are place in the grid, edge articulations are computed so that edges are + * routed orthogonally using an {@link OrthogonalEdgeRouter}. + *

+ * + * 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 the vertex type + * @param the edge type + */ + +public abstract class AbstractFlowChartLayout> + extends AbstractVisualGraphLayout { + protected Comparator edgeComparator; + protected boolean leftAligned; + + protected AbstractFlowChartLayout(DefaultVisualGraph graph, + Comparator edgeComparator, boolean leftAligned) { + super(graph, "Flow Chart"); + this.edgeComparator = edgeComparator; + this.leftAligned = leftAligned; + } + + @Override + protected GridLocationMap performInitialGridLayout(VisualGraph g) + throws CancelledException { + + V root = getRoot(g); + + GDirectedGraph tree = GraphAlgorithms.toTree(g, root, edgeComparator); + GridLocationMap grid = computeGridLocationMap(g, tree, root); + + OrthogonalEdgeRouter router = new OrthogonalEdgeRouter<>(grid); + router.setColumnExclusionFunction(e -> getExcludedCols(grid, tree, e)); + router.computeAndSetEdgeArticulations(g.getEdges()); + + return grid; + } + + @Override + protected LayoutPositions positionInLayoutSpaceFromGrid(VisualGraph g, + GridLocationMap grid) throws CancelledException { + + boolean isCondensed = isCondensedLayout(); + Function transformer = new VisualGraphVertexShapeTransformer<>(); + + OrthogonalGridToLayoutMapper layoutMap = + new OrthogonalGridToLayoutMapper(grid, transformer, isCondensed); + + Map vertexMap = layoutMap.getVertexLocations(); + Map> edgeMap = layoutMap.getEdgeLocations(vertexMap); + + LayoutPositions 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 g); + + @Override + public boolean usesEdgeArticulations() { + return true; + } + + @Override + protected Point2D getVertexLocation(V v, Column col, Row 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 computeGridLocationMap(GDirectedGraph g, + GDirectedGraph tree, V v) { + + Collection edges = tree.getOutEdges(v); + + if (edges.isEmpty()) { + GridLocationMap grid = new GridLocationMap<>(v, 1, 1); + return grid; + } + + // get all child grids and merge them side by side + + List sortedEdges = new ArrayList<>(edges); + sortedEdges.sort(edgeComparator); + E edge = sortedEdges.get(0); + V child = edge.getEnd(); + int totalEdges = sortedEdges.size(); + + GridLocationMap 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 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 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 leftGrid, GridLocationMap 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 getVisualGraph() { + return (VisualGraph) 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 grid, GDirectedGraph 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 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 tree, V v) { + Collection predecessors = tree.getPredecessors(v); + if (predecessors == null || predecessors.isEmpty()) { + return null; + } + return predecessors.iterator().next(); + } +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColSegmentList.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColSegmentList.java new file mode 100644 index 0000000000..888749033e --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColSegmentList.java @@ -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 edge type + */ +public class ColSegmentList { + private List> 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 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> groups = sortIntoNonOverlappingGroups(edgeSegments); + for (ColSegmentList 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 other) { + if (minY > other.maxY) { + return false; + } + if (other.minY > maxY) { + return false; + } + return true; + } + + ColumnSegment getSegment(E edge, GridPoint startPoint) { + for (ColumnSegment 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 edgeSegment : edgeSegments) { + minOffset = Math.min(minOffset, edgeSegment.getOffset()); + } + return minOffset; + } + + int getMaxOffset() { + int maxOffset = 0; + for (EdgeSegment edgeSegment : edgeSegments) { + maxOffset = Math.max(maxOffset, edgeSegment.getOffset()); + } + return maxOffset; + } + + void addSegment(ColumnSegment segment) { + edgeSegments.add(segment); + minY = Math.min(minY, segment.getVirtualMinY()); + maxY = Math.max(maxY, segment.getVirtualMaxY()); + } + + private void assignOffsets(ColSegmentList 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 segment : group.edgeSegments) { + segment.setOffset(segment.getOffset() + adjustment); + } + } + } + + private void assignOffsets(ColSegmentList group, int center) { + List> nonOverlappingSegments = new ArrayList<>(); + + // assign negative offsets to column segments to the left of the center segment. + for (int i = center; i >= 0; i--) { + ColumnSegment 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 segment = group.edgeSegments.get(i); + assignOffsets(nonOverlappingSegments, segment, 2); // 2 to keep edges two offsets apart + } + } + + private void assignOffsets(List> nonOverlappingSegments, + ColumnSegment 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(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 segment) { + for (ColumnSegment edgeSegment : edgeSegments) { + if (segment.overlaps(edgeSegment)) { + return true; + } + } + return false; + + } + + private int findNaturalCenter(ColSegmentList group) { + for (int i = 0; i < group.edgeSegments.size(); i++) { + ColumnSegment edgeSegment = group.edgeSegments.get(i); + if (edgeSegment.points.size() == 2) { + return i; + } + } + return -1; + } + + private List> sortIntoNonOverlappingGroups(List> segments) { + List> groups = new ArrayList<>(segments.size()); + for (ColumnSegment segment : segments) { + groupSegment(groups, segment); + } + return groups; + } + + private void groupSegment(List> groups, ColumnSegment segment) { + ColSegmentList newGroup = new ColSegmentList(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 segmentList) { + edgeSegments.addAll(segmentList.edgeSegments); + minY = Math.min(minY, segmentList.minY); + maxY = Math.max(maxY, segmentList.maxY); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColumnSegment.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColumnSegment.java new file mode 100644 index 0000000000..aec1da0263 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/ColumnSegment.java @@ -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 The edge type + */ +public class ColumnSegment extends EdgeSegment implements Comparable> { + + // 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 next; + private RowSegment 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 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 previous, E e, List points, int pointIndex) { + super(e, points, pointIndex); + this.previous = previous; + if (pointIndex < points.size() - 2) { + next = new RowSegment(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 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 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 myTopRowSegment = getTopRowSegment(); + RowSegment 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 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 myBottomRowSegment = getBottomRowSegment(); + RowSegment 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 other) { + if (getVirtualMinY() > other.getVirtualMaxY()) { + return false; + } + if (getVirtualMaxY() < other.getVirtualMinY()) { + return false; + } + return true; + } + + @Override + public RowSegment nextSegment() { + return next; + } + + @Override + public RowSegment 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 getTopRowSegment() { + return flowsUp() ? next : previous; + } + + private RowSegment getBottomRowSegment() { + return flowsUp() ? previous : next; + } + + private RowOrientation getOrientationForTopRow() { + if (isStartSegment()) { + return RowOrientation.TERMINAL; + } + RowSegment 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 bottomRowSegment = getBottomRowSegment(); + int bottomRowOtherCol = + flowsUp() ? bottomRowSegment.getStartCol() : bottomRowSegment.getEndCol(); + return bottomRowOtherCol < getCol() ? RowOrientation.LEFT : RowOrientation.RIGHT; + } + + private boolean flowsUp() { + return getStartRow() > getEndRow(); + } + + public ColumnSegment last() { + if (isEndSegment()) { + return this; + } + return next.last(); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegment.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegment.java new file mode 100644 index 0000000000..6bfc6e19d6 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegment.java @@ -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. + *

+ * 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. + *

+ * 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 the edge type + */ +public abstract class EdgeSegment { + + protected E edge; + protected List 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 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 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 previousSegment(); +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentMap.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentMap.java new file mode 100644 index 0000000000..f7b490a93d --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentMap.java @@ -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. + *

+ * The offsets have to be computed before sizing the grid because the offsets affect + * the size of the grid rows and columns. + * + * @param the edge type + */ +public class EdgeSegmentMap { + + private Map> rowSegmentMap = new HashMap<>(); + private Map> colSegmentMap = new HashMap<>(); + + public EdgeSegmentMap(GridLocationMap grid) { + createEdgeSegments(grid); + assignEdgeSegmentOffsets(); + } + + /** + * Returns a collection of all edge row segment lists. + * @return a collection of all edge row segment lists + */ + public Collection> rowSegments() { + return rowSegmentMap.values(); + } + + /** + * Returns a collection of all edge column segment lists. + * @return a collection of all edge column segment lists + */ + public Collection> 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 getColumnSegment(E edge, GridPoint gridPoint) { + ColSegmentList colSegments = colSegmentMap.get(gridPoint.col); + return colSegments == null ? null : colSegments.getSegment(edge, gridPoint); + } + + public void dispose() { + rowSegmentMap.clear(); + colSegmentMap.clear(); + } + + private void createEdgeSegments(GridLocationMap grid) { + for (E edge : grid.edges()) { + + List gridPoints = grid.getArticulations(edge); + ColumnSegment colSegment = new ColumnSegment(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 rowSegment = colSegment.nextSegment(); + addRowSegment(rowSegment); + colSegment = rowSegment.nextSegment(); + addColSegment(colSegment); + } + } + } + + private void assignEdgeSegmentOffsets() { + for (RowSegmentList rowSegments : rowSegmentMap.values()) { + rowSegments.assignOffsets(); + } + for (ColSegmentList colSegments : colSegmentMap.values()) { + colSegments.assignOffsets(); + } + } + + private void addRowSegment(RowSegment rowSegment) { + int row = rowSegment.getRow(); + RowSegmentList edgeRow = + rowSegmentMap.computeIfAbsent(row, k -> new RowSegmentList(k)); + edgeRow.addSegment(rowSegment); + } + + private void addColSegment(ColumnSegment colSegment) { + int col = colSegment.getCol(); + ColSegmentList edgeCol = + colSegmentMap.computeIfAbsent(col, k -> new ColSegmentList(k)); + edgeCol.addSegment(colSegment); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FGFlowChartLayout.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FGFlowChartLayout.java new file mode 100644 index 0000000000..1d924a65cc --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FGFlowChartLayout.java @@ -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 + 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 createClonedLayout( + VisualGraph newGraph) { + return new FGFlowChartLayout((FunctionGraph) newGraph, leftAligned); + } + + @Override + public FGLayout cloneLayout(VisualGraph newGraph) { + VisualGraphLayout clone = super.cloneLayout(newGraph); + return (FGLayout) clone; + } + + @Override + protected boolean isCondensedLayout() { + return options.useCondensedLayout(); + } + + @Override + public BasicEdgeRenderer getEdgeRenderer() { + return new FGEdgeRenderer(); + } + + private static class FGEdgeComparator implements Comparator { + @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 g) { + if (graph instanceof FunctionGraph fg) { + return fg.getRootVertex(); + } + return null; + } +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutProvider.java new file mode 100644 index 0000000000..162c4caf3d --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutProvider.java @@ -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); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutProvider.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutProvider.java new file mode 100644 index 0000000000..c9f253baf3 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutProvider.java @@ -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); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalEdgeRouter.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalEdgeRouter.java new file mode 100644 index 0000000000..2b070fc086 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalEdgeRouter.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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 the vertex type + * @param the edge type + */ + +public class OrthogonalEdgeRouter> { + private Function excludedColumnsFunction; + private GridLocationMap grid; + private Map> columnMap; + + public OrthogonalEdgeRouter(GridLocationMap 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 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 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 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 column = columnMap.get(col); + if (column == null) { + return true; + } + return column.isOpenBetween(startRow, endRow); + } + + private List getEdgePoints(GridPoint p1, GridPoint p2, int routingCol) { + List points = new ArrayList(); + + 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; + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridSizer.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridSizer.java new file mode 100644 index 0000000000..ddbc7e8df1 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridSizer.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 the vertex type + * @param the edge type + */ +public class OrthogonalGridSizer { + 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 gridMap, EdgeSegmentMap segmentMap, + Function 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 gridMap, EdgeSegmentMap segmentMap) { + for (ColSegmentList 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 gridMap, EdgeSegmentMap segmentMap) { + // edge rows have no vertices, so its height just depends on the number of parallel edges + for (RowSegmentList rowSegments : segmentMap.rowSegments()) { + int row = rowSegments.getRow(); + int edgeCount = rowSegments.getMaxOffset() - rowSegments.getMinOffset(); + addRowHeight(row, edgeCount * edgeSpacing); + } + } + + private void addVertexSizes(GridLocationMap gridMap, Function transformer) { + Map vertexPoints = gridMap.getVertexPoints(); + + for (Entry 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); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridToLayoutMapper.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridToLayoutMapper.java new file mode 100644 index 0000000000..00e87f1c2a --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/OrthogonalGridToLayoutMapper.java @@ -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. + *

+ * 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 the vertex type + * @param the edge type + */ +public class OrthogonalGridToLayoutMapper> { + private static final int EDGE_SPACING = 7; + private static final int CONDENSED_EDGE_SPACING = 5; + + private EdgeSegmentMap segmentMap; + private GridLocationMap grid; + private Function transformer; + private GridCoordinates gridCoordinates; + private int edgeSpacing; + + public OrthogonalGridToLayoutMapper(GridLocationMap grid, Function 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(grid); + + // compute row and column sizes + OrthogonalGridSizer 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 getVertexLocations() { + + Map vertexLocations = new HashMap<>(); + + for (Entry 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> getEdgeLocations(Map vertexLocations) { + Map> edgeLocations = new HashMap<>(); + for (E edge : grid.edges()) { + + List gridPoints = grid.getArticulations(edge); + List points = new ArrayList<>(gridPoints.size()); + + GridPoint gp = gridPoints.get(0); + EdgeSegment 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 getEdgeOffsets(E edge) { + List offsets = new ArrayList<>(); + List gridPoints = grid.getArticulations(edge); + + GridPoint gp = gridPoints.get(0); + EdgeSegment 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; + } +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegment.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegment.java new file mode 100644 index 0000000000..548c3d3856 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegment.java @@ -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 The edge type + */ +public class RowSegment extends EdgeSegment implements Comparable> { + // 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 next; + private ColumnSegment previous; + + RowSegment(ColumnSegment previous, E e, List points, int pointIndex) { + super(e, points, pointIndex); + this.previous = previous; + // row segments always have a follow-on column segment + this.next = new ColumnSegment(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 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 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 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 myLeftColSegment = getLeftColSegment(); + ColumnSegment 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 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 myRightColSegment = getRightColSegment(); + ColumnSegment otherRightColSegment = other.getRightColSegment(); + + if (myRightColOrientation == ColumnOrientation.UP) { + return myRightColSegment.compareTops(otherRightColSegment); + } + return -myRightColSegment.compareBottoms(otherRightColSegment); + } + + @Override + public ColumnSegment nextSegment() { + return next; + } + + @Override + public ColumnSegment previousSegment() { + return previous; + } + + private ColumnOrientation getOrientationForLeftColumn() { + ColumnSegment leftSegment = getLeftColSegment(); + int leftColOtherRow = flowsLeft() ? leftSegment.getEndRow() : leftSegment.getStartRow(); + return leftColOtherRow < getRow() ? ColumnOrientation.UP : ColumnOrientation.DOWN; + } + + private ColumnOrientation getOrientationForRightColumn() { + ColumnSegment rightSegment = getRightColSegment(); + int rightColOtherRow = flowsLeft() ? rightSegment.getStartRow() : rightSegment.getEndRow(); + return rightColOtherRow < getRow() ? ColumnOrientation.UP : ColumnOrientation.DOWN; + } + + private ColumnSegment getLeftColSegment() { + return flowsLeft() ? next : previous; + } + + private ColumnSegment getRightColSegment() { + return flowsLeft() ? previous : next; + } + + private boolean flowsLeft() { + return getStartCol() > getEndCol(); + } + + private boolean isLeftTerminal() { + ColumnSegment left = getLeftColSegment(); + return left.isStartSegment() || left.isEndSegment(); + } + + private boolean isRightTerminal() { + ColumnSegment right = getRightColSegment(); + return right.isStartSegment() || right.isEndSegment(); + } + + public ColumnSegment last() { + return next.last(); + } +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegmentList.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegmentList.java new file mode 100644 index 0000000000..105f6aecff --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/RowSegmentList.java @@ -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 edge type + */ +public class RowSegmentList { + protected List> edgeSegments = new ArrayList<>(); + int row; + + public RowSegmentList(int row) { + this.row = row; + } + + protected void addSegment(RowSegment 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 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 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> offsetMap = + LazyMap.lazyMap(new HashMap<>(), k -> new RowSegmentList(0)); + for (int i = 0; i < edgeSegments.size(); i++) { + RowSegment segment = edgeSegments.get(i); + assignOffset(offsetMap, segment); + } + } + + protected void assignOffset(Map> offsetMap, RowSegment segment) { + + // assigning offsets to rows is easy, just find the first offset group that we don't + // overlap with. + + int offset = 0; + RowSegmentList segments = offsetMap.get(offset); + + while (segments.hasOverlappingSegment(segment)) { + offset += 2; + segments = offsetMap.get(offset); + } + segment.setOffset(offset); + segments.addSegment(segment); + } + + private boolean hasOverlappingSegment(RowSegment segment) { + for (RowSegment edgeSegment : edgeSegments) { + if (segment.overlaps(edgeSegment)) { + return true; + } + } + return false; + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/jungrapht/JgtNamedLayout.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/jungrapht/JgtNamedLayout.java index 5940bf0e6f..a831a4667e 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/jungrapht/JgtNamedLayout.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/jungrapht/JgtNamedLayout.java @@ -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 row, + protected Point2D getVertexLocation(FGVertex v, Column col, Row 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 newPoints = new ArrayList<>(); + List newPoints = new ArrayList<>(); List 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 diff --git a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/graph/viewer/FGViewUpdater.java b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/graph/viewer/FGViewUpdater.java index 4493cbe896..8fabca6a12 100644 --- a/Ghidra/Features/FunctionGraph/src/main/java/ghidra/graph/viewer/FGViewUpdater.java +++ b/Ghidra/Features/FunctionGraph/src/main/java/ghidra/graph/viewer/FGViewUpdater.java @@ -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 { 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 // diff --git a/Ghidra/Features/FunctionGraph/src/main/resources/images/function_graph_flowchart.png b/Ghidra/Features/FunctionGraph/src/main/resources/images/function_graph_flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b6480e0b408f44c6e36bd52b7f087c8abcfa56 GIT binary patch literal 972 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GXl4nEnR%gt#iUC~Ecqk)|OKO)<2b z14Nci)yJHcT6zJ|TBo3%b3s6It5;N%Wz?K&Q9#DlpyWCCl7Zx2Ad1Rf`aBy*?oBQR zg0;_!k0n=EM^$fqUwy2&rKP23@AsZ#)jj80rX2e|)^mS!_ z!NV-huYLKn;w_+%ho_5Uh{WaO1P10rMmayW6omzgj)^mS!_ z!NV-B#DD(L{3k#mH%}MG5Q)pl2?v;3SSu_7Sy)1*+z4RVA@VdLfW_iQ#Ed}RKvt(F zri2ES2A@R1fT row, + protected Point2D getVertexLocation(FGVertex v, Column col, Row 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 startCol = layoutLocations.col(startVertex); + Column endCol = layoutLocations.col(endVertex); Point2D start = vertexLayoutLocations.get(startVertex); Point2D end = vertexLayoutLocations.get(endVertex); List articulations = new ArrayList<>(); diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayoutTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayoutTest.java new file mode 100644 index 0000000000..b9e5442f31 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/AbstractFlowChartLayoutTest.java @@ -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 grid; + protected OrthogonalGridToLayoutMapper 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 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 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 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 expectedPoints = new ArrayList<>(); + private List 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 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 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 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 transformer = v -> new Rectangle(0, 0, 50, 50); + layoutMap = + new OrthogonalGridToLayoutMapper(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 { + + private AbstractTestVertex root; + + protected TestFlowChartLayout(DefaultVisualGraph graph, + AbstractTestVertex root, boolean leftAlign) { + super(graph, new TestEdgeComparator(), leftAlign); + this.root = root; + } + + @Override + public AbstractVisualGraphLayout createClonedLayout( + VisualGraph newGraph) { + throw new UnsupportedOperationException(); + } + + @Override + protected AbstractTestVertex getRoot(VisualGraph g) { + return root; + } + + @Override + protected GridLocationMap performInitialGridLayout( + VisualGraph g) throws CancelledException { + + return super.performInitialGridLayout(g); + } + + } + + private static class TestEdgeComparator implements Comparator { + + @Override + public int compare(TestEdge e1, TestEdge e2) { + return e1.toString().compareTo(e2.toString()); + } + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java new file mode 100644 index 0000000000..6816c5ef8b --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/FlowChartLayoutTest.java @@ -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))); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutTest.java new file mode 100644 index 0000000000..868ef105a3 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/LeftAlignedFlowChartLayoutTest.java @@ -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))); + } + +} diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java new file mode 100644 index 0000000000..52ac1992c7 --- /dev/null +++ b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/graph/layout/flowchart/EdgeSegmentTest.java @@ -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 col1 = segment(e12, p(0, 1), DOWN); + ColumnSegment col2 = segment(e13, p(0, 2), DOWN); + + assertLessThan(col1, col2); + assertGreaterThan(col2, col1); + } + + @Test + public void testCompareStartSegmentByDirectionOfBottomRow() { + GridPoint p = p(0, 0); + ColumnSegment down = segment(e12, p, DOWN_2); + ColumnSegment left = segment(e13, p, DOWN, LEFT, DOWN); + ColumnSegment 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 down1_left = segment(e13, p, DOWN, LEFT, DOWN); + ColumnSegment 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 down1_right = segment(e13, p, DOWN, RIGHT, DOWN); + ColumnSegment 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 leftUp = segment(e13, p, DOWN, LEFT, UP); + ColumnSegment leftDown = segment(e14, p, DOWN, LEFT, DOWN); + + assertLessThan(leftUp, leftDown); + assertGreaterThan(leftDown, leftUp); + + } + + @Test + public void testCompareStartBothRightsThenOppositeDirection() { + GridPoint p = p(0, 0); + + ColumnSegment rightUp = segment(e13, p, DOWN, RIGHT, UP); + ColumnSegment rightDown = segment(e14, p, DOWN, RIGHT, DOWN); + + assertLessThan(rightDown, rightUp); + assertGreaterThan(rightUp, rightDown); + + } + + @Test + public void testCompareLeftDownButDifferentLeftColumn() { + GridPoint p = p(0, 0); + + ColumnSegment left1 = segment(e13, p, DOWN, LEFT, DOWN); + ColumnSegment left2 = segment(e14, p, DOWN, LEFT_2, DOWN); + + assertLessThan(left2, left1); + assertGreaterThan(left1, left2); + } + + @Test + public void testCompareLeftUpButDifferentLeftColumn() { + GridPoint p = p(0, 0); + + ColumnSegment left1 = segment(e13, p, DOWN, LEFT, UP); + ColumnSegment left2 = segment(e14, p, DOWN, LEFT_2, UP); + + assertLessThan(left1, left2); + assertGreaterThan(left2, left1); + } + + @Test + public void testCompareRightDownButDifferentRightColumn() { + GridPoint p = p(0, 0); + + ColumnSegment right1 = segment(e13, p, DOWN, RIGHT, DOWN); + ColumnSegment right2 = segment(e14, p, DOWN, RIGHT_2, DOWN); + + assertLessThan(right1, right2); + assertGreaterThan(right2, right1); + } + + @Test + public void testCompareRightUpButDifferentRightColumn() { + GridPoint p = p(0, 0); + + ColumnSegment right1 = segment(e13, p, DOWN, RIGHT, UP); + ColumnSegment right2 = segment(e14, p, DOWN, RIGHT_2, UP); + + assertLessThan(right2, right1); + assertGreaterThan(right1, right2); + } + + @Test + public void testCompareLeftUpButUpperRowDifferentDirections() { + GridPoint p = p(0, 0); + + ColumnSegment l_u_left = segment(e13, p, DOWN, LEFT, UP, LEFT); + ColumnSegment 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 l_d_left = segment(e13, p, DOWN, LEFT, DOWN, LEFT); + ColumnSegment 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 r_u_left = segment(e13, p, DOWN, RIGHT, UP, LEFT); + ColumnSegment 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 r_d_left = segment(e13, p, DOWN, RIGHT, DOWN, LEFT); + ColumnSegment 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 l_u_left = segment(e13, p, DOWN, LEFT, UP, LEFT, DOWN); + ColumnSegment 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 l_u_right = segment(e13, p, DOWN, LEFT, UP, RIGHT, DOWN); + ColumnSegment 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 l_d_left = segment(e13, p, DOWN, LEFT, DOWN, LEFT, DOWN); + ColumnSegment 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 l_d_right = segment(e13, p, DOWN, LEFT, DOWN, RIGHT, DOWN); + ColumnSegment 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 r_u_left = segment(e13, p, DOWN, RIGHT, UP, LEFT, DOWN); + ColumnSegment 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 r_u_right = segment(e13, p, DOWN, RIGHT, UP, RIGHT, DOWN); + ColumnSegment 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 r_d_left = segment(e13, p, DOWN, RIGHT, DOWN, LEFT, DOWN); + ColumnSegment 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 r_d_right = segment(e13, p, DOWN, RIGHT, DOWN, RIGHT, DOWN); + ColumnSegment 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 col0 = endSegment(e12, p1, UP_2); + ColumnSegment col1 = endSegment(e12, p2, UP_2); + assertLessThan(col0, col1); + assertGreaterThan(col1, col0); + } + + @Test + public void testCompareEndsFromDifferentDirections() { + GridPoint p = p(5, 5); + + ColumnSegment fromRight = endSegment(e14, p, UP, RIGHT, UP); + ColumnSegment fromAbove = endSegment(e24, p, UP); + ColumnSegment 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 up1_left = endSegment(e13, p, UP, LEFT, UP); + ColumnSegment 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 up1_right = endSegment(e13, p, UP, RIGHT, UP); + ColumnSegment 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 u_l_up = endSegment(e13, p, UP, LEFT, UP); + ColumnSegment 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 u_r_up = endSegment(e13, p, UP, RIGHT, UP); + ColumnSegment 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 left1 = endSegment(e13, p, UP, LEFT, DOWN); + ColumnSegment left2 = endSegment(e14, p, UP, LEFT_2, DOWN); + + assertLessThan(left1, left2); + assertGreaterThan(left2, left1); + } + + @Test + public void testCompareEndsLeftUpButDifferentLeftColumn() { + GridPoint p = p(0, 0); + + ColumnSegment left1 = endSegment(e13, p, UP, LEFT, UP); + ColumnSegment left2 = endSegment(e14, p, UP, LEFT_2, UP); + + assertLessThan(left2, left1); + assertGreaterThan(left1, left2); + } + + @Test + public void testCompareEndsRightDownButDifferentRightColumn() { + GridPoint p = p(0, 0); + + ColumnSegment right1 = endSegment(e13, p, UP, RIGHT, DOWN); + ColumnSegment right2 = endSegment(e14, p, UP, RIGHT_2, DOWN); + + assertLessThan(right2, right1); + assertGreaterThan(right1, right2); + } + + @Test + public void testCompareEndsRightUpButDifferentRightColumn() { + GridPoint p = p(0, 0); + + ColumnSegment right1 = endSegment(e13, p, UP, RIGHT, UP); + ColumnSegment right2 = endSegment(e14, p, UP, RIGHT_2, UP); + + assertLessThan(right1, right2); + assertGreaterThan(right2, right1); + } + + @Test + public void testCompareEndsLeftUpButUpperRowDifferentDirections() { + GridPoint p = p(0, 0); + + ColumnSegment l_u_left = endSegment(e13, p, UP, LEFT, UP, LEFT, UP); + ColumnSegment 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 l_d_left = endSegment(e13, p, UP, LEFT, DOWN, LEFT, UP); + ColumnSegment 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 r_u_left = endSegment(e13, p, UP, RIGHT, UP, LEFT, UP); + ColumnSegment 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 r_d_left = endSegment(e13, p, UP, RIGHT, DOWN, LEFT, UP); + ColumnSegment 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 l_u_left = endSegment(e13, p, UP, LEFT, UP, LEFT, UP); + ColumnSegment 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 l_u_right = endSegment(e13, p, UP, LEFT, UP, RIGHT, UP); + ColumnSegment 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 l_d_left = endSegment(e13, p, UP, LEFT, DOWN, LEFT, UP); + ColumnSegment 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 l_d_right = endSegment(e13, p, UP, LEFT, DOWN, RIGHT, UP); + ColumnSegment 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 r_u_left = endSegment(e13, p, UP, RIGHT, UP, LEFT, UP); + ColumnSegment 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 r_u_right = endSegment(e13, p, UP, RIGHT, UP, RIGHT, UP); + ColumnSegment 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 r_d_left = endSegment(e13, p, UP, RIGHT, DOWN, LEFT, UP); + ColumnSegment 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 r_d_right = endSegment(e13, p, UP, RIGHT, DOWN, RIGHT, UP); + ColumnSegment 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 colSeg1 = segment(e13, p, DOWN, RIGHT, DOWN, RIGHT, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, RIGHT, DOWN, RIGHT_2, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, RIGHT, DOWN, LEFT_2, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, RIGHT, DOWN, LEFT, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, RIGHT, UP, RIGHT, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, RIGHT, UP, RIGHT_2, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, RIGHT, UP, LEFT_2, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, RIGHT, UP, LEFT, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, LEFT, DOWN, RIGHT, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, LEFT, DOWN, RIGHT_2, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, LEFT, DOWN, LEFT_2, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, LEFT, DOWN, LEFT, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, LEFT, UP, RIGHT, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, LEFT, UP, RIGHT_2, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p, DOWN, LEFT, UP, LEFT_2, DOWN); + ColumnSegment colSeg2 = segment(e14, p, DOWN, LEFT, UP, LEFT, DOWN); + assertLessThan(colSeg1, colSeg2); + assertGreaterThan(colSeg2, colSeg1); + + RowSegment rowSeg1 = colSeg1.nextSegment(); + RowSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN); + ColumnSegment colSeg2 = segment(e14, p(100, 0), DOWN); + assertFalse(colSeg1.overlaps(colSeg2)); + assertFalse(colSeg2.overlaps(colSeg1)); + } + + @Test + public void testColumnSegmentEndDoesNotOverlapColumnSegmentStartToSameVertex() { + ColumnSegment colSeg1 = segment(e13, p(0, 0), DOWN); + ColumnSegment colSeg2 = segment(e14, p(2, 0), DOWN); + assertFalse(colSeg1.overlaps(colSeg2)); + } + + @Test + public void testColumnStartSegmentToSharedRowFromEndSegmentDependsOnRowOffsets() { + ColumnSegment colSeg1 = segment(e13, p(0, 1), DOWN, RIGHT, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN, LEFT, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN, LEFT, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT_2, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 0), DOWN, RIGHT, DOWN, RIGHT, DOWN); + ColumnSegment 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 colSeg1 = segment(e13, p(0, 1), DOWN, LEFT, DOWN, LEFT, DOWN); + ColumnSegment 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 s1, ColumnSegment s2) { + int result = s1.compareTo(s2); + if (result != 0) { + fail("Expected comparsion to be equals, but compareTo was " + result); + } + } + + private void assertLessThan(ColumnSegment s1, ColumnSegment s2) { + int result = s1.compareTo(s2); + if (result >= 0) { + fail("Expected comparsion to be less than, but compareTo was " + result); + } + } + + private void assertGreaterThan(RowSegment s1, RowSegment s2) { + int result = s1.compareTo(s2); + if (result <= 0) { + fail("Expected comparsion to be greater than, but compareTo was " + result); + } + } + + private void assertLessThan(RowSegment s1, RowSegment s2) { + int result = s1.compareTo(s2); + if (result >= 0) { + fail("Expected comparsion to be less than, but compareTo was " + result); + } + } + + private void assertGreaterThan(ColumnSegment s1, ColumnSegment s2) { + int result = s1.compareTo(s2); + if (result <= 0) { + fail("Expected comparsion to be greater than, but compareTo was " + result); + } + } + + private ColumnSegment segment(TestEdge e, GridPoint p, int... flows) { + return new ColumnSegment<>(e, points(p, flows)); + } + + private ColumnSegment 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 points(GridPoint start, int... flows) { + List 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 pointsReverseOrder(GridPoint end, int... flows) { + List 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); + } + +} diff --git a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java index eda8176d35..dfd9cc3f9f 100644 --- a/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java +++ b/Ghidra/Features/FunctionGraphDecompilerExtension/src/main/java/ghidra/app/plugin/core/functiongraph/graph/layout/DecompilerNestedLayout.java @@ -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. @@ -171,9 +171,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout { Address entryPoint = function.getEntryPoint(); FGVertex vertex = getVertex(jungGraph, entryPoint); - Integer row = gridLocations.row(vertex); - Integer col = gridLocations.col(vertex); - if (row != 0 || col != 0) { + GridPoint gridPoint = gridLocations.gridPoint(vertex); + if (gridPoint.row != 0 && gridPoint.col != 0) { Msg.debug(this, "Function graph has entry point not at top of layout: " + entryPoint); } @@ -338,7 +337,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { int startColumn = Math.min(start.columnIndex, end.columnIndex); int endColumn = Math.max(start.columnIndex, end.columnIndex); - Column rightmostLoopColumn = layoutToGridMap.col(rightmostLoopVertex); + Column rightmostLoopColumn = layoutToGridMap.col(rightmostLoopVertex); endColumn = Math.max(endColumn, rightmostLoopColumn.index); // Look for any vertices that are no part of the loop, but are placed inside @@ -351,8 +350,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout { // place the right x position to the right of the rightmost vertex, not // extending past the next column FGVertex rightmostVertex = getRightmostVertex(interlopers); - Column rightmostColumn = layoutToGridMap.col(rightmostVertex); - Column nextColumn = layoutToGridMap.nextColumn(rightmostColumn); + Column rightmostColumn = layoutToGridMap.col(rightmostVertex); + Column nextColumn = layoutToGridMap.nextColumn(rightmostColumn); Vertex2d rightmostV2d = vertex2dFactory.get(rightmostVertex); // the padding used for these two lines is somewhat arbitrary and may be changed @@ -646,7 +645,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { } else { // going up we swing out to the right; grab the column that is out to the right - Column rightColumn = vertex2dFactory.getColumn(edgeX); + Column rightColumn = vertex2dFactory.getColumn(edgeX); endColumn = rightColumn.index; } @@ -855,7 +854,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { } @Override - protected Point2D getVertexLocation(FGVertex v, Column col, Row row, + protected Point2D getVertexLocation(FGVertex v, Column col, Row row, Rectangle bounds) { return getCenteredVertexLocation(v, col, row, bounds); } @@ -1128,7 +1127,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { this.edgeOffset = edgeOffset; } - Column getColumn(double x) { + Column getColumn(double x) { return layoutToGridMap.getColumnContaining((int) x); } @@ -1163,7 +1162,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout { private FGVertex v; private Row row; - private Column column; + private Column column; private int rowIndex; private int columnIndex; private Point2D center; // center point of vertex shape diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java index e4b1dc2982..79a6893532 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/layout/BowTieLayout.java @@ -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. @@ -63,7 +63,7 @@ public class BowTieLayout extends AbstractVisualGraphLayout } @Override - protected Point2D getVertexLocation(FcgVertex v, Column col, Row row, + protected Point2D getVertexLocation(FcgVertex v, Column col, Row row, Rectangle bounds) { return getCenteredVertexLocation(v, col, row, bounds); } diff --git a/Ghidra/Framework/Generic/src/main/java/util/CollectionUtils.java b/Ghidra/Framework/Generic/src/main/java/util/CollectionUtils.java index 756368fadb..4574e3e3de 100644 --- a/Ghidra/Framework/Generic/src/main/java/util/CollectionUtils.java +++ b/Ghidra/Framework/Generic/src/main/java/util/CollectionUtils.java @@ -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. @@ -485,4 +485,21 @@ public class CollectionUtils { } return null; } + + /** + * Returns the only element from the given collection; null if the collection is null or empty + * or size is greater than 1. This is meant to clients to get the one and only element in + * a collection of size 1. + * + * @param c the collection + * @return the item + * @see #any(Collection) + */ + public static T get(Collection c) { + if (c == null || c.size() > 1) { + return null; + } + return any((Iterable) c); + } + } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java index b8061dd9e2..5c849b8f9d 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphAlgorithms.java @@ -627,4 +627,48 @@ public class GraphAlgorithms { recursivePrint(g, v2, set, depth + 1, ps); } } + + /** + * Returns a list of list of vertices sorted topologically such that for every edge + * V1 -> V2, the V1 vertex will appear in the resulting list before the V2 vertex. Normally, + * this is only defined for acyclic graphs. For purposes of this implementation, a root vertex + * is given as a start point and any edge encountered by following edges from the root that + * results in a "back" edge (i.e any edge that points to a previously visited vertex) is + * ignored, effectively making the graph acyclic (somewhat arbitrarily depending the order in + * which vertexes are visited which is determined by the given edge comparator). Also, note + * that any vertex in the graph that is not reachable from the given root will not appear in + * the resulting list of sorted vertices. + * + * @param the vertex type + * @param the edge type + * @param g the graph + * @param root the start node for traversing the graph (will always be the first node in the + * resulting list) + * @param edgeComparator provides an ordering for traversing the graph which can impact which + * edges are ignored as "back" edges and ultimately affect the final ordering + * @return a list of vertices reachable from the given root vertex, sorted topologically + */ + public static > List topologicalSort(GDirectedGraph g, V root, + Comparator edgeComparator) { + GraphToTreeAlgorithm algorithm = new GraphToTreeAlgorithm(g, edgeComparator); + return algorithm.topolocigalSort(root); + } + + /** + * Converts a general directed graph into a tree graph with the given vertex as the root. It + * does this by first doing a topological sort (which ignores back edges) and greedily accepting + * the first incoming edge based on the sorted vertex order. + * @param the vertex type + * @param the edge type + * @param g the graph to be converted into a tree + * @param root the vertex to be used as the root + * @param edgeComparator provides a priority ordering of edges with higher priority edges + * getting first shot at claiming children for its sub-tree. + * @return a graph with edges removed such that the graph is a tree. + */ + public static > GDirectedGraph toTree(GDirectedGraph g, + V root, Comparator edgeComparator) { + GraphToTreeAlgorithm algorithm = new GraphToTreeAlgorithm(g, edgeComparator); + return algorithm.toTree(root); + } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphToTreeAlgorithm.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphToTreeAlgorithm.java new file mode 100644 index 0000000000..17eb8090b6 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/GraphToTreeAlgorithm.java @@ -0,0 +1,216 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph; + +import java.util.*; + +import org.apache.commons.collections4.map.LazyMap; + +import ghidra.graph.jung.JungDirectedGraph; + +/** + * This class provides an algorithm for topological graph sorting and an algorithm for using + * that topological sort to create a tree structure from the graph using that topological sort. + *

+ * In general topological sorting and converting to a tree, require an acyclic graph. However, + * by supplying a root vertex, the graph can be made to be acyclic by traversing the graph from + * that root and discarding any edges the return to a "visited" vertex. This has a side effect of + * ignoring any nodes that are not reachable from the root node. Also, this algorithm class is + * constructed with an edge comparator which can also determine the order nodes are traversed, + * thereby affecting the final ordering or tree structure. Higher priority edges will be processed + * first, making those edges least likely to be removed as "back" edges. + *

+ * To convert a general graph to a tree, some subset of the the graphs original edges are used to + * form the tree. There are many possible different trees that can be created in this way. This + * algorimth's goal is to create a tree such that if all the original "forward" edges are added + * back to the tree, they only flow down the tree. This is useful for creating a nicely organized + * layout of vertices and edges when drawn. + * + * @param The vertex type + * @param The edge type + */ +public class GraphToTreeAlgorithm> { + private GDirectedGraph graph; + private Comparator edgeComparator; + + /** + * Constructor. + * + * @param graph the graph from with to create a tree + * @param edgeComparator provides a priority ordering of edges with higher priority edges + * getting first shot at claiming children for its sub-tree. + */ + public GraphToTreeAlgorithm(GDirectedGraph graph, Comparator edgeComparator) { + this.graph = graph; + this.edgeComparator = edgeComparator; + } + + /** + * Creates a tree graph with the given vertex as the root from this object's graph. + * + * @param root the vertex to be used as the root + * getting first shot at claiming children for its sub-tree. + * @return a graph with edges removed such that the graph is a tree. + */ + public GDirectedGraph toTree(V root) { + + // first sort the vertices topologically + List sorted = topolocigalSort(root); + + // Visit nodes in the sorted order and track the longest path to each node from the root. + Map depthMap = assignDepths(root, sorted); + + // Assign vertices to the tree in the sorted order and only using edges where the "from" + // vertex (parent) is at a depth 1 less then the depth of "to" vertex. This will ensure + // that the tree is ordered such that if all the original forward edges are added back in, + // they would always flow down the tree. + return createTree(root, sorted, depthMap); + + } + + /** + * Sorts the vertices in this graph topologically. + * + * @param root the start node for traversing the graph (will always be the first node in the + * resulting list) + * @return a list of vertices reachable from the given root vertex, sorted topologically + */ + public List topolocigalSort(V root) { + + Set visited = new HashSet<>(); + List ordered = new ArrayList<>(); + + Deque stack = new ArrayDeque<>(); + + stack.push(new VertexChildIterator(root)); + visited.add(root); + + while (!stack.isEmpty()) { + VertexChildIterator childIterator = stack.getFirst(); + if (childIterator.hasNext()) { + V child = childIterator.next(); + + // only process the child if never seen before, otherwise it is a loop back + if (!visited.contains(child)) { + stack.push(new VertexChildIterator(child)); + visited.add(child); + } + } + else { + ordered.add(childIterator.getParent()); + stack.pop(); + } + } + Collections.reverse(ordered); + return ordered; + } + + private JungDirectedGraph createTree(V root, List sorted, Map depthMap) { + Set visited = new HashSet<>(); + visited.add(root); + + JungDirectedGraph tree = new JungDirectedGraph(); + for (V v : sorted) { + tree.addVertex(v); + } + + for (V parent : sorted) { + Depth parentDepth = depthMap.get(parent); + Collection outEdges = graph.getOutEdges(parent); + for (E e : outEdges) { + V child = e.getEnd(); + if (visited.contains(child)) { + continue; // already assigned + } + Depth childDepth = depthMap.get(child); + if (childDepth.isDirectChildOf(parentDepth)) { + tree.addEdge(e); + visited.add(child); + } + } + } + return tree; + } + + private Map assignDepths(V root, List sorted) { + + Set visited = new HashSet<>(); + Map depthMap = LazyMap.lazyMap(new HashMap<>(), k -> new Depth()); + + depthMap.put(root, new Depth()); + for (V parent : sorted) { + visited.add(parent); + Depth parentDepth = depthMap.get(parent); + List edges = new ArrayList<>(); + Collection out = graph.getOutEdges(parent); + if (out != null) { + edges.addAll(out); + } + edges.sort(edgeComparator); + for (E e : edges) { + V child = e.getEnd(); + if (visited.contains(child)) { + continue; // loop backs are ignored + } + Depth childDepth = depthMap.get(child); + childDepth.adjustDepth(parentDepth); + } + } + return depthMap; + } + + // traces the distance from the root of the tree + private static class Depth { + private int depth = 0; + + private void adjustDepth(Depth parentDepth) { + depth = Math.max(depth, parentDepth.depth + 1); + } + + private boolean isDirectChildOf(Depth parentDepth) { + return depth == parentDepth.depth + 1; + } + } + + private class VertexChildIterator { + private V parent; + private Iterator it; + + VertexChildIterator(V parent) { + this.parent = parent; + Collection out = graph.getOutEdges(parent); + List outEdges = new ArrayList<>(); + if (out != null) { + outEdges.addAll(out); + } + outEdges.sort(edgeComparator); + it = outEdges.reversed().iterator(); + } + + V getParent() { + return parent; + } + + public boolean hasNext() { + return it.hasNext(); + } + + public V next() { + return it.next().getEnd(); + } + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java index a3b51fcbde..011c2c75f1 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java @@ -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,7 +627,7 @@ public class GraphComponent, G e return satelliteViewer; } - protected VisualGraphViewUpdater getViewUpdater() { + public VisualGraphViewUpdater getViewUpdater() { return primaryViewer.getViewUpdater(); } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/VisualGraphViewUpdater.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/VisualGraphViewUpdater.java index a2dc43f5ac..47406d7db3 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/VisualGraphViewUpdater.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/VisualGraphViewUpdater.java @@ -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. @@ -335,8 +335,9 @@ public class VisualGraphViewUpdater(primaryViewer, isAnimationEnabled())); + } + } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java index ab778c3fe8..40621761b3 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/AbstractVisualGraphLayout.java @@ -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. @@ -337,7 +337,7 @@ public abstract class AbstractVisualGraphLayout row = layoutLocations.row(vertex); - Column column = layoutLocations.col(vertex); + Column column = layoutLocations.col(vertex); Shape shape = transformer.apply(vertex); Rectangle bounds = shape.getBounds(); @@ -366,7 +366,7 @@ public abstract class AbstractVisualGraphLayout row, Rectangle bounds) { + protected Point2D getVertexLocation(V v, Column col, Row row, Rectangle bounds) { int x = col.x - bounds.x; int y = row.y - bounds.y; return new Point2D.Double(x, y); @@ -381,7 +381,7 @@ public abstract class AbstractVisualGraphLayout row, Rectangle bounds) { + protected Point2D getCenteredVertexLocation(V v, Column col, Row row, Rectangle bounds) { // // Move x over to compensate for vertex painting. Edges are drawn from the center of the // vertex. Thus, if you have vertices with two different widths, then the edge between @@ -409,9 +409,9 @@ public abstract class AbstractVisualGraphLayout newArticulations = new ArrayList<>(); - for (Point gridPoint : layoutLocations.articulations(edge)) { - Row row = layoutLocations.row(gridPoint.y); - Column column = layoutLocations.col(gridPoint.x); + for (GridPoint gridPoint : layoutLocations.articulations(edge)) { + Row row = layoutLocations.row(gridPoint.row); + Column column = layoutLocations.col(gridPoint.col); Point2D location = getEdgeLocation(column, row); newArticulations.add(location); @@ -421,11 +421,11 @@ public abstract class AbstractVisualGraphLayout row) { + protected Point2D getEdgeLocation(Column col, Row row) { return new Point2D.Double(col.x, row.y); } - protected Point2D getCenteredEdgeLocation(Column col, Row row) { + protected Point2D getCenteredEdgeLocation(Column col, Row row) { // // half-height offsets the articulation points, which keeps long edge lines from // overlapping as much diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Column.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Column.java index 59ab7045c3..0b2fe6008b 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Column.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Column.java @@ -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. @@ -16,16 +16,22 @@ package ghidra.graph.viewer.layout; import java.awt.geom.Point2D; +import java.util.*; +import java.util.Map.Entry; import ghidra.graph.viewer.GraphViewerUtils; /** - * A row in a grid. This class stores it's row index, its x offset and its width. The + * A column in a grid. This class stores its column index, its x offset and its width. The * x value is the layout space x value of a {@link Point2D} object. That is, unlike the * {@link GridLocationMap}, the x value of this object is in layout space and not indexes * of a grid. + * + *

This class maintains a collection of vertices on this column, organized by column index. You + * can get the column of a vertex from {@link #getRow(Object)}. + * @param The vertex type */ -public class Column { +public class Column { /** The layout x coordinate of the column */ public int x = -1; @@ -33,11 +39,26 @@ public class Column { /** The grid index of this column (0, 1...n) for the number of columns */ public int index = Integer.MAX_VALUE; + // Note: these must change together (they are effectively a BiDi map) + private TreeMap verticesByRow = new TreeMap<>(); + private Map rowsByVertex = new HashMap<>(); public Column(int index) { this.index = index; } + public void setRow(V v, int row) { + rowsByVertex.put(v, row); + verticesByRow.put(row, v); + } + + public int getRow(V v) { + if (!rowsByVertex.containsKey(v)) { + throw new IllegalArgumentException("Vertex is not in row: " + v); + } + return rowsByVertex.get(v); + } + public int getPaddedWidth(boolean isCondensed) { if (isCondensed) { return width + GraphViewerUtils.EXTRA_LAYOUT_COLUMN_SPACING_CONDENSED; @@ -61,4 +82,13 @@ public class Column { "}"; //@formatter:on } + + public boolean isOpenBetween(int startRow, int endRow) { + Entry ceilingEntry = verticesByRow.ceilingEntry(startRow); + if (ceilingEntry == null) { + return true; + } + int nextRow = ceilingEntry.getKey(); + return nextRow > endRow; + } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java new file mode 100644 index 0000000000..be56cec4ef --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridBounds.java @@ -0,0 +1,100 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph.viewer.layout; + +/** + * Tracks the minimum and maximum indexes for both rows and columns. + */ + +public class GridBounds { + private int minRow = 0; + private int maxRow = 0; + private int minCol = 0; + private int maxCol = 0; + + /** + * Updates the bounds for the given GridPoint. + * @param p the gridPoint used to update the minimums and maximums + */ + public void update(GridPoint p) { + minRow = Math.min(minRow, p.row); + maxRow = Math.max(maxRow, p.row); + minCol = Math.min(minCol, p.col); + maxCol = Math.max(maxCol, p.col); + } + + /** + * Shifts the columns bounds by the given amount + * @param rowShift the amount to shift the row bounds. + * @param colShift the amount to shift the column bounds. + * @throws IllegalArgumentException if the shift would make the minimum column negative + */ + public void shift(int rowShift, int colShift) { + minCol += colShift; + maxCol += colShift; + minRow += rowShift; + maxRow += rowShift; + } + + @Override + public String toString() { + StringBuilder buffy = new StringBuilder(); + buffy.append("Grid Bounds: "); + if (minRow == Integer.MAX_VALUE) { + return "Empty"; + } + + buffy.append("rows: ").append(minRow).append(" -> ").append(maxRow); + buffy.append(", "); + buffy.append("cols: ").append(minCol).append(" -> ").append(maxCol); + return buffy.toString(); + } + + public int maxCol() { + return maxCol; + } + + public int minCol() { + // handle case when grid is empty + if (minCol > maxCol) { + return 0; + } + return minCol; + } + + public int maxRow() { + return maxRow; + } + + public int minRow() { + // handle case when grid is empty + if (minRow > maxRow) { + return 0; + } + return minRow; + } + + public boolean contains(GridPoint p) { + if (p.row < minRow || p.row > maxRow) { + return false; + } + if (p.col < minCol || p.col > maxCol) { + return false; + } + return true; + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridCoordinates.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridCoordinates.java new file mode 100644 index 0000000000..83b38e2a97 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridCoordinates.java @@ -0,0 +1,79 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph.viewer.layout; + +import java.awt.Rectangle; + +/** + * Tracks the mapping of grid coordinates (rows, columns) to space coordinates (x, y) + */ +public class GridCoordinates { + private int[] rowStarts; + private int[] colStarts; + + /** + * Constructor + * @param rowCoordinates an array containing the y locations for all rows in a grid + * @param columnCoordinates an array containing the x locations for all columns in a grid + */ + public GridCoordinates(int[] rowCoordinates, int[] columnCoordinates) { + rowStarts = rowCoordinates; + colStarts = columnCoordinates; + } + + /** + * Returns the x value for a given column. + * @param col the column index in the grid + * @return the x coordinate assigned to the given column index + */ + public int x(int col) { + return colStarts[col]; + } + + /** + * Returns the y value for a given row. + * @param row the row index in the grid + * @return the y coordinate assigned to the given row index + */ + public int y(int row) { + return rowStarts[row]; + } + + /** + * Returns the total bounds for the grid + * @return the total bounds for the grid + */ + public Rectangle getBounds() { + return new Rectangle(0, 0, colStarts[colStarts.length - 1], + rowStarts[rowStarts.length - 1]); + } + + /** + * returns the number of rows in the grid. + * @return the number of rows in the grid + */ + public int rowCount() { + return rowStarts.length; + } + + /** + * returns the number of columns in the grid. + * @return the number of columns in the grid + */ + public int columnCount() { + return colStarts.length; + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java index 65b0f74954..4ae8660a19 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridLocationMap.java @@ -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. @@ -15,49 +15,86 @@ */ package ghidra.graph.viewer.layout; -import java.awt.Point; import java.util.*; import java.util.Map.Entry; +import java.util.stream.Stream; -import org.apache.commons.collections4.Factory; import org.apache.commons.collections4.map.LazyMap; /** - * An object that maps vertices to rows and columns and edges to their articulation points. - * This class is essentially a container that allows layout algorithms to store results, which - * can later be turned into layout positioning points. The integer point values in this - * class are row, column grid values, starting at 0,0. - * - *

Note: the Point2D values for the edge articulations use x,y values that are row and - * column index values, the same values as calling {@link #row(Object) row(V)} and {@link #col(Object) col(V)}. - * - *

After building the grid using this class, clients can call {@link #rows()} to get - * high-order object that represent rows. + * An object that maps vertices and edge articulation points to rows and columns in a grid. This + * class is essentially a container that allows layout algorithms to store results as it lays + * out vertices and edges in a virtual grid. Later, this information can be used in conjunction + * with vertex size information and padding information to transform these grid coordinates to + * layout space coordinates. + *

+ * This object also has methods for manipulating the grid such as shifting it up, down, left, right, + * and merging in other GridLocationMaps + *

+ * After building the grid using this class, clients can call {@link #rows()}, {@link #rowsMap()}, + * or {@link #columnsMap()} to get high-order objects that represent rows or columns. * * @param the vertex type * @param the edge type */ public class GridLocationMap { - private Factory rowColFactory = () -> new Point(); + protected Map vertexPoints = + LazyMap.lazyMap(new HashMap(), v -> new GridPoint(0, 0)); - private Map vertexPoints = LazyMap.lazyMap(new HashMap(), rowColFactory); - private Map> edgePoints = new HashMap<>(); + protected Map> edgePoints = new HashMap<>(); + private GridBounds gridBounds = new GridBounds(); + // Tree based algorithms might want to track the column of the root node as it changes when + // the grid is shifted or merged.Useful for determining the position of a parent node when + // building bottom up. + private int rootColumn = 0; - Set vertices() { + public GridLocationMap() { + rootColumn = 0; + } + + /** + * Constructor that includes an initial "root" vertex. + * @param root the initial vertex + * @param row the row for the initial vertex + * @param col the column for the initial vertex. + */ + public GridLocationMap(V root, int row, int col) { + this.rootColumn = col; + set(root, new GridPoint(row, col)); + } + + /** + * Returns the column of the initial vertex in this grid. + * @return the column of the initial vertex in this grid + */ + public int getRootColumn() { + return rootColumn; + } + + public Set vertices() { return vertexPoints.keySet(); } - Set edges() { + public Set edges() { return edgePoints.keySet(); } - public void setArticulations(E edge, List articulations) { - edgePoints.put(edge, articulations); + public Map getVertexPoints() { + return vertexPoints; } - public List getArticulations(E edge) { - List list = edgePoints.get(edge); + public void setArticulations(E edge, List articulations) { + edgePoints.put(edge, articulations); + if (articulations != null) { + for (GridPoint gridPoint : articulations) { + gridBounds.update(gridPoint); + } + } + } + + public List getArticulations(E edge) { + List list = edgePoints.get(edge); if (list == null) { return Collections.emptyList(); } @@ -65,25 +102,44 @@ public class GridLocationMap { } public void row(V vertex, int row) { - vertexPoints.get(vertex).y = row; + GridPoint gridPoint = vertexPoints.get(vertex); + gridPoint.row = row; + gridBounds.update(gridPoint); } public void col(V vertex, int col) { - vertexPoints.get(vertex).x = col; + GridPoint gridPoint = vertexPoints.get(vertex); + gridPoint.col = col; + gridBounds.update(gridPoint); } public void set(V v, int row, int col) { - Point p = vertexPoints.get(v); - p.x = col; - p.y = row; + set(v, new GridPoint(row, col)); + } + + public void set(V v, GridPoint gridPoint) { + vertexPoints.put(v, gridPoint); + gridBounds.update(gridPoint); } public int row(V vertex) { - return vertexPoints.get(vertex).y; + GridPoint gridPoint = vertexPoints.get(vertex); + if (gridPoint != null) { + return gridPoint.row; + } + return 0; } public int col(V vertex) { - return vertexPoints.get(vertex).x; + GridPoint gridPoint = vertexPoints.get(vertex); + if (gridPoint != null) { + return gridPoint.col; + } + return 0; + } + + public GridPoint gridPoint(V vertex) { + return vertexPoints.get(vertex); } /** @@ -92,143 +148,82 @@ public class GridLocationMap { * @return the rows in this grid */ public List> rows() { - - Map> rowsByIndex = new HashMap<>(); - - Set> entrySet = vertexPoints.entrySet(); - for (Entry entry : entrySet) { - V v = entry.getKey(); - Point gridPoint = entry.getValue(); - int rowIndex = gridPoint.y; - Row row = getRow(rowsByIndex, rowIndex); - row.index = rowIndex; - row.setColumn(v, gridPoint.x); - } - + Map> rowsByIndex = rowsMap(); List> rows = new ArrayList<>(rowsByIndex.values()); rows.sort((r1, r2) -> r1.index - r2.index); return rows; } - private Row getRow(Map> rows, int rowIndex) { - Row row = rows.get(rowIndex); - if (row == null) { - row = new Row<>(rowIndex); - rows.put(rowIndex, row); + /** + * Returns a mapping or row indexes to Row objects in this grid + * + * @return the rows in this grid + */ + public Map> rowsMap() { + Map> rowsByIndex = LazyMap.lazyMap(new HashMap<>(), r -> new Row(r)); + + Set> entrySet = vertexPoints.entrySet(); + for (Entry entry : entrySet) { + V v = entry.getKey(); + GridPoint gridPoint = entry.getValue(); + int rowIndex = gridPoint.row; + Row row = rowsByIndex.get(rowIndex); + row.setColumn(v, gridPoint.col); } - return row; + return rowsByIndex; } /** - * Updates each row within the grid such that it's x values are set to center the row in + * Returns a mapping or column indexes to Column objects in this grid + * + * @return the columns in this grid + */ + public Map> columnsMap() { + Map> columnsMap = + LazyMap.lazyMap(new HashMap<>(), c -> new Column(c)); + + Set> entrySet = vertexPoints.entrySet(); + for (Entry entry : entrySet) { + V v = entry.getKey(); + GridPoint gridPoint = entry.getValue(); + int colIndex = gridPoint.col; + Column col = columnsMap.get(colIndex); + col.setRow(v, gridPoint.row); + } + return columnsMap; + } + + /** + * Updates each row within the grid such that it's column values are set to center the row in * the grid. Each row will be updated so that all its columns start at zero. After that, * each column will be centered in the grid. */ public void centerRows() { + zeroAlignGrid(); + GridRange[] vertexColumnRanges = getVertexColumnRanges(); + int maxRowWidth = getMaxRowWidth(vertexColumnRanges); - List> rows = rows(); - int maxCol = columnCount(rows); - for (Row row : rows) { - - row = zeroRowColumns(row); - - int rowColumnCount = row.getColumnCount(); - if (rowColumnCount == maxCol) { - continue; // already the full size; no need to center - } - - int delta = maxCol - rowColumnCount; - int offset = delta / 2; - List vertices = row.getVertices(); - for (V v : vertices) { - if (v == null) { - continue; - } - - int oldCol = col(v); - set(v, row.index, oldCol + offset); - } - - row.dispose(); + for (GridPoint p : allPoints()) { + GridRange range = vertexColumnRanges[p.row]; + int extraSpace = maxRowWidth - range.width(); + int shift = extraSpace / 2 - range.min; + p.col += shift; } } - private int maxColumnIndex(List> rows) { - int maxCol = 0; - for (Row row : rows) { - maxCol = Math.max(maxCol, row.getEndColumn()); + private int getMaxRowWidth(GridRange[] vertexColumnRanges) { + int maxWidth = 0; + for (GridRange gridRange : vertexColumnRanges) { + maxWidth = Math.max(maxWidth, gridRange.width()); } - return maxCol; + return maxWidth; } - private int maxRowIndex(List> rows) { - int maxRow = 0; - for (Row row : rows) { - maxRow = Math.max(maxRow, row.index); - } - return maxRow; - } - - private int columnCount(List> rows) { - - int maxCount = 0; - for (Row row : rows) { - maxCount = Math.max(maxCount, row.getColumnCount()); - } - return maxCount; - } - -// private int rowCount(List> rows) { -// int minRow = 0; -// int maxRow = 0; -// for (Row row : rows) { -// minRow = Math.min(minRow, row.index); -// maxRow = Math.max(maxRow, row.index); -// } -// return (maxRow - minRow) + 1; // +1 for zero-based -// } - - private Row zeroRowColumns(Row row) { - - int start = row.getStartColumn(); - int offset = -start; - - Row updatedRow = new Row<>(); - updatedRow.index = row.index; - for (V v : row.getVertices()) { - int oldCol = col(v); - int newCol = oldCol + offset; - set(v, row.index, newCol); - updatedRow.setColumn(v, newCol); - } - - row.dispose(); - return updatedRow; - } - - GridLocationMap copy() { - GridLocationMap map = new GridLocationMap<>(); - - map.vertexPoints = new HashMap<>(); - Set> entries = vertexPoints.entrySet(); - for (Entry entry : entries) { - map.vertexPoints.put(entry.getKey(), (Point) entry.getValue().clone()); - } - - map.edgePoints = new HashMap<>(); - Set>> edgeEntries = edgePoints.entrySet(); - for (Entry> entry : edgeEntries) { - - List points = entry.getValue(); - List clonedPoints = new ArrayList<>(points.size()); - for (Point p : points) { - clonedPoints.add((Point) p.clone()); - } - - map.edgePoints.put(entry.getKey(), clonedPoints); - } - - return map; + /** + * Shifts the grid so that its first row and column are at 0. + */ + public void zeroAlignGrid() { + shift(-gridBounds.minRow(), -gridBounds.minCol()); } public void dispose() { @@ -236,6 +231,103 @@ public class GridLocationMap { edgePoints.clear(); } + /** + * Shifts the rows and columns for all points in this map by the given amount. + * @param rowShift the amount to shift the rows of each point + * @param colShift the amount to shift the columns of each point + */ + public void shift(int rowShift, int colShift) { + if (rowShift == 0 && colShift == 0) { + return; + } + + for (GridPoint p : allPoints()) { + p.row += rowShift; + p.col += colShift; + } + rootColumn += colShift; + gridBounds.shift(rowShift, colShift); + + } + + /** + * Returns the number of rows in this grid map. Note that this includes empty rows + * starting at the 0 row. + * @return the number of rows in this grid map + */ + public int height() { + return gridBounds.maxRow() + 1; + } + + /** + * Returns the number of columns in this grid map. Note that this includes empty columns + * starting at the 0 column. + * @return the number of columns in this grid map + */ + public int width() { + return gridBounds.maxCol() + 1; + } + + /** + * Returns the minimum/max column for all rows in the grid. This method is only defined for + * grids that have no negative rows. This is because the array returned will be 0 based, with + * the entry at index 0 containing the column bounds for row 0 and so on. + * @return the minimum/max column for all rows in the grid + * @throws IllegalStateException if this method is called on a grid with negative rows. + */ + public GridRange[] getVertexColumnRanges() { + if (gridBounds.minRow() < 0) { + throw new IllegalStateException( + "getVertexColumnRanges not defined for grids with negative rows!"); + } + GridRange[] rowRanges = new GridRange[height()]; + + for (int i = 0; i < rowRanges.length; i++) { + rowRanges[i] = new GridRange(); + } + + for (GridPoint p : vertexPoints.values()) { + rowRanges[p.row].add(p.col); + } + return rowRanges; + } + + public boolean containsVertex(V v) { + return vertexPoints.containsKey(v); + } + + public boolean containsEdge(E e) { + return edgePoints.containsKey(e); + } + + /** + * Adds in the vertices and edges from another GridLocationMap with each point in the other + * grid map shifted by the given row and column amounts. + * @param other the other GridLocationMap to add to this one. + * @param rowShift the amount to shift the rows in the grid points from the other grid before + * adding them to this grid + * @param colShift the amount to shift the columns in the grid points from the other grid before + * adding them to this grid + */ + public void add(GridLocationMap other, int rowShift, int colShift) { + + for (Entry entry : other.vertexPoints.entrySet()) { + V v = entry.getKey(); + GridPoint point = entry.getValue(); + set(v, new GridPoint(point.row + rowShift, point.col + colShift)); + } + + for (Entry> entry : other.edgePoints.entrySet()) { + E e = entry.getKey(); + List points = entry.getValue(); + List shiftedPoints = new ArrayList<>(points.size()); + for (GridPoint point : points) { + shiftedPoints.add(new GridPoint(point.row + rowShift, point.col + colShift)); + } + setArticulations(e, shiftedPoints); + } + } + @Override public String toString() { return getClass().getSimpleName() + "[\n\tvertex points=" + vertexPoints + @@ -247,66 +339,71 @@ public class GridLocationMap { * @return a string representation of this grid */ public String toStringGrid() { - - GridLocationMap copy = copy(); - zeroAlignGrid(copy); - - List> rows = copy.rows(); - int columnCount = copy.maxColumnIndex(rows) + 1; - int rowCount = copy.maxRowIndex(rows) + 1; - - Object[][] vGrid = new Object[rowCount][columnCount]; - for (Row row : rows) { - List vertices = row.getVertices(); - for (V v : vertices) { - vGrid[row.index][row.getColumn(v)] = v; - } + int minRow = gridBounds.minRow(); + int minCol = gridBounds.minCol(); + if (minRow > 10 || minCol > 10) { + GridLocationMap copy = copy(); + copy.zeroAlignGrid(); + return "grid upper left (row,col) = (" + minRow + ", " + minCol + ")\n" + + copy.toStringGrid(); } - StringBuilder buffy = new StringBuilder("\n"); - for (int row = 0; row < rowCount; row++) { - for (int col = 0; col < columnCount; col++) { - Object o = vGrid[row][col]; - buffy.append(' '); - if (o == null) { - buffy.append('-'); - //buffy.append(' '); - } - else { - buffy.append('v'); - } - buffy.append(' '); - } - buffy.append('\n'); - } + String[][] vGrid = new String[height()][width()]; + for (Entry entry : vertexPoints.entrySet()) { + V v = entry.getKey(); + GridPoint p = entry.getValue(); + vGrid[p.row][p.col] = normalizeVertexName(v.toString()); + } + StringBuilder buffy = new StringBuilder(); + buffy.append("\n"); + for (int row = 0; row < vGrid.length; row++) { + for (int col = 0; col < vGrid[row].length; col++) { + String name = vGrid[row][col]; + name = name == null ? ". " : name; + buffy.append(name); + buffy.append(""); + } + buffy.append("\n"); + } return buffy.toString(); } - // moves all rows and columns as needed to convert the grid origin to 0,0 - private static void zeroAlignGrid(GridLocationMap grid) { + private GridLocationMap copy() { + GridLocationMap map = new GridLocationMap<>(); + map.rootColumn = rootColumn; - int smallestColumnIndex = 0; - int smallestRowIndex = 0; - List> rows = grid.rows(); - for (Row row : rows) { - smallestRowIndex = Math.min(smallestRowIndex, row.index); - smallestColumnIndex = Math.min(smallestColumnIndex, row.getStartColumn()); + Set> entries = vertexPoints.entrySet(); + for (Entry entry : entries) { + map.set(entry.getKey(), new GridPoint(entry.getValue())); } - int globalColumnOffset = -smallestColumnIndex; - int globalRowOffset = -smallestRowIndex; - - for (Row row : rows) { - - List vertices = row.getVertices(); - for (V v : vertices) { - int oldCol = grid.col(v); - int oldRow = grid.row(v); - int newCol = globalColumnOffset + oldCol; - int newRow = globalRowOffset + oldRow; - grid.set(v, newRow, newCol); - } + Set>> edgeEntries = edgePoints.entrySet(); + for (Entry> entry : edgeEntries) { + List points = entry.getValue(); + List copy = new ArrayList<>(points.size()); + points.forEach(p -> copy.add(new GridPoint(p))); + map.setArticulations(entry.getKey(), copy); } + + return map; + } + + private String normalizeVertexName(String name) { + if (name.length() > 8) { + return name.substring(0, 8); + } + return name + " ".substring(name.length()); + } + + private Iterable allPoints() { + Stream vPoints = vertexPoints.values().stream(); + Stream ePoints = edgePoints.values().stream().flatMap(l -> l.stream()); + Stream streams = Stream.concat(vPoints, ePoints); + return () -> streams.iterator(); + } + + public boolean containsPoint(GridPoint p) { + return gridBounds.contains(p); } } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java new file mode 100644 index 0000000000..dfdb71b804 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridPoint.java @@ -0,0 +1,64 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph.viewer.layout; + +import java.util.Objects; + +/** + * Row and column information for points in a {@link GridLocationMap}. Using these instead + * of java Points, makes the code that translates from grid space to layout space much less + * confusing. + */ +public class GridPoint { + + public int row; + public int col; + + public GridPoint(int row, int col) { + this.row = row; + this.col = col; + } + + public GridPoint(GridPoint point) { + this.row = point.row; + this.col = point.col; + } + + @Override + public int hashCode() { + return Objects.hash(col, row); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GridPoint other = (GridPoint) obj; + return col == other.col && row == other.row; + } + + @Override + public String toString() { + return "(r=" + row + ",c=" + col + ")"; + } +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridRange.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridRange.java new file mode 100644 index 0000000000..9722aaddb8 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/GridRange.java @@ -0,0 +1,78 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph.viewer.layout; + +import java.util.Objects; + +/** + * Class for reporting the min/max columns in a row or the min/max rows in a column + */ +public class GridRange { + public int min; + public int max; + + public GridRange() { + this(Integer.MAX_VALUE, Integer.MIN_VALUE); + } + + public GridRange(int min, int max) { + this.min = min; + this.max = max; + } + + public void add(int value) { + min = Math.min(value, min); + max = Math.max(value, max); + } + + @Override + public String toString() { + return "[" + min + " -> " + max + "]"; + } + + public boolean isEmpty() { + return min > max; + } + + public boolean contains(int value) { + return value >= min && value <= max; + } + + @Override + public int hashCode() { + return Objects.hash(max, min); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GridRange other = (GridRange) obj; + return max == other.max && min == other.min; + } + + public int width() { + if (isEmpty()) { + return 0; + } + return max - min + 1; + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java index f301405d17..1ceab2a107 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/LayoutLocationMap.java @@ -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. @@ -15,9 +15,9 @@ */ package ghidra.graph.viewer.layout; -import java.awt.*; +import java.awt.Rectangle; +import java.awt.Shape; import java.util.*; -import java.util.List; import java.util.Map.Entry; import com.google.common.base.Function; @@ -43,7 +43,7 @@ public class LayoutLocationMap { private int numColumns; private TreeMap> rowsByIndex = new TreeMap<>(); - private TreeMap columnsByIndex = new TreeMap<>(); + private TreeMap> columnsByIndex = new TreeMap<>(); private boolean isCondensed = false; private GridLocationMap gridLocations; @@ -54,29 +54,12 @@ public class LayoutLocationMap { this.gridLocations = gridLocations; Set vertices = gridLocations.vertices(); - Set edges = gridLocations.edges(); - MinMaxRowColumn minMax = getMinMaxRowColumnValues(vertices, edges, monitor); - numRows = minMax.maxRow + 1; - numColumns = minMax.maxCol + 1; + numRows = gridLocations.height(); + numColumns = gridLocations.width(); initializeLayoutLocations(transformer, vertices, monitor); } - private LayoutLocationMap() { - // copy constructor - } - - public LayoutLocationMap copy() { - LayoutLocationMap map = new LayoutLocationMap<>(); - map.isCondensed = isCondensed; - map.numRows = numRows; - map.numColumns = numColumns; - map.rowsByIndex = new TreeMap<>(rowsByIndex); - map.columnsByIndex = new TreeMap<>(columnsByIndex); - map.gridLocations = gridLocations.copy(); - return map; - } - public void dispose() { rowsByIndex.clear(); columnsByIndex.clear(); @@ -90,19 +73,19 @@ public class LayoutLocationMap { return numColumns; } - public Column col(V v) { + public Column col(V v) { Integer col = gridLocations.col(v); return doGetColumn(col); } - public Column col(int gridX) { + public Column col(int gridX) { return doGetColumn(gridX); } - public Column getColumnContaining(int x) { - Column column = null; - Collection values = columnsByIndex.values(); - for (Column nextColumn : values) { + public Column getColumnContaining(int x) { + Column column = null; + Collection> values = columnsByIndex.values(); + for (Column nextColumn : values) { if (x < nextColumn.x) { return column; } @@ -111,10 +94,10 @@ public class LayoutLocationMap { return column; } - private Column doGetColumn(int index) { - Column column = columnsByIndex.get(index); + private Column doGetColumn(int index) { + Column column = columnsByIndex.get(index); if (column == null) { - column = new Column(index); + column = new Column<>(index); columnsByIndex.put(index, column); } return column; @@ -125,10 +108,10 @@ public class LayoutLocationMap { * * @return the columns in this location map, sorted from lowest index to highest */ - public Collection columns() { - List result = new ArrayList<>(); - Collection values = columnsByIndex.values(); - for (Column column : values) { + public Collection> columns() { + List> result = new ArrayList<>(); + Collection> values = columnsByIndex.values(); + for (Column column : values) { result.add(column); } return result; @@ -148,17 +131,17 @@ public class LayoutLocationMap { return results; } - public Column lastColumn() { + public Column lastColumn() { - Entry lastEntry = columnsByIndex.lastEntry(); + Entry> lastEntry = columnsByIndex.lastEntry(); if (lastEntry == null) { return null; } return lastEntry.getValue(); } - public Column nextColumn(Column column) { - Column nextColumn = doGetColumn(column.index + 1); + public Column nextColumn(Column column) { + Column nextColumn = doGetColumn(column.index + 1); if (!nextColumn.isInitialized()) { // last column? nextColumn.x = column.x + column.getPaddedWidth(isCondensed); @@ -166,12 +149,12 @@ public class LayoutLocationMap { return nextColumn; } - public List articulations(E e) { + public List articulations(E e) { return gridLocations.getArticulations(e); } public Row row(V v) { - Integer row = gridLocations.row(v); + int row = gridLocations.row(v); return doGetRow(row); } @@ -215,7 +198,7 @@ public class LayoutLocationMap { public List getColOffsets() { ArrayList list = new ArrayList<>(); - for (Column column : columnsByIndex.values()) { + for (Column column : columnsByIndex.values()) { list.add(column.x); } return list; @@ -231,70 +214,55 @@ public class LayoutLocationMap { columnsByIndex + "]"; } + public GridCoordinates getGridCoordinates() { + Row lastRow = lastRow(); + Column lastColumn = lastColumn(); + if (lastRow == null || lastColumn == null) { + return new GridCoordinates(new int[0], new int[0]); + } + + // add 1 to compute a row y value and a column x value for closing the grid + int[] rowStarts = new int[lastRow.index + 1]; + int[] colStarts = new int[lastColumn.index + 1]; + + for (Row row : rowsByIndex.values()) { + rowStarts[row.index] = row.y; + } + for (Column col : columnsByIndex.values()) { + colStarts[col.index] = col.x; + } + + // Give any empty rows or columns the coordinate of the row or column that precedes it + // since it takes no space. (Otherwise all the empty row or column labels would overwrite + // themselves at the 0 row or 0 column. + for (int row = 1; row < rowStarts.length; row++) { + if (rowStarts[row] == 0) { + rowStarts[row] = rowStarts[row - 1]; + } + } + for (int col = 1; col < colStarts.length; col++) { + if (colStarts[col] == 0) { + colStarts[col] = colStarts[col - 1]; + } + } + + // close the grid + rowStarts[rowStarts.length - 1] = lastRow.y + lastRow.getPaddedHeight(isCondensed); + colStarts[colStarts.length - 1] = lastColumn.x + lastColumn.getPaddedWidth(isCondensed); + + return new GridCoordinates(rowStarts, colStarts); + + } + //================================================================================================== // Initialization Code //================================================================================================== - private MinMaxRowColumn getMinMaxRowColumnValues(Collection vertices, Collection edges, - TaskMonitor monitor) throws CancelledException { - - MinMaxRowColumn minMax = new MinMaxRowColumn(); - - for (V v : vertices) { - monitor.checkCancelled(); - - int row = gridLocations.row(v); - if (row > minMax.maxRow) { - minMax.maxRow = row; - } - if (row < minMax.minRow) { - minMax.minRow = row; - } - - int column = gridLocations.col(v); - if (column > minMax.maxCol) { - minMax.maxCol = column; - } - if (column < minMax.minCol) { - minMax.minCol = column; - } - } - - for (E edge : edges) { - monitor.checkCancelled(); - - List articulations = gridLocations.getArticulations(edge); - if (articulations.isEmpty()) { - continue; - } - - for (Point location : articulations) { - int row = location.y; - if (row > minMax.maxRow) { - minMax.maxRow = row; - } - if (row < minMax.minRow) { - minMax.minRow = row; - } - - int column = location.x; - if (column > minMax.maxCol) { - minMax.maxCol = column; - } - if (column < minMax.minCol) { - minMax.minCol = column; - } - } - } - - return minMax; - } - private void initializeLayoutLocations(Function transformer, Collection vertices, TaskMonitor monitor) throws CancelledException { // create this class's rows from the grid - List> gridRows = gridLocations.rows(); + Collection> gridRows = gridLocations.rowsMap().values(); for (Row row : gridRows) { rowsByIndex.put(row.index, row); } @@ -308,7 +276,7 @@ public class LayoutLocationMap { monitor.checkCancelled(); Row row = row(vertex); - Column column = col(vertex); + Column column = col(vertex); Shape shape = transformer.apply(vertex); Rectangle bounds = shape.getBounds(); if (bounds.width > column.width) { @@ -349,7 +317,7 @@ public class LayoutLocationMap { for (int i = 0; i < n; i++) { monitor.checkCancelled(); - Column column = col(i); + Column column = col(i); column.x = offset; offset += column.getPaddedWidth(isCondensed); } diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/MinMaxRowColumn.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/MinMaxRowColumn.java deleted file mode 100644 index 7bb8e961ca..0000000000 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/MinMaxRowColumn.java +++ /dev/null @@ -1,23 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.graph.viewer.layout; - -public class MinMaxRowColumn { - public int minRow = Integer.MAX_VALUE; - public int maxRow = -1; - public int minCol = Integer.MAX_VALUE; - public int maxCol = -1; -} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Row.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Row.java index 9a3e1eec73..05dabf8638 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Row.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/layout/Row.java @@ -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. @@ -29,7 +29,7 @@ import ghidra.graph.viewer.GraphViewerUtils; * of a grid. * *

This class maintains a collection of vertices on this row, organized by column index. You - * can get the column of a vertex from {@link #getColumn(Object) getColumn(V)}. + * can get the column of a vertex from {@link #getColumn(Object)} * * @param the vertex type */ @@ -42,7 +42,7 @@ public class Row { /** The grid index of this row (0, 1...n) for the number of rows */ public int index = Integer.MAX_VALUE; - // Note: this must change together (they are effectively a BiDi map) + // Note: these must change together (they are effectively a BiDi map) private TreeMap verticesByColumn = new TreeMap<>(); private Map columnsByVertex = new HashMap<>(); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/GridPainter.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/GridPainter.java new file mode 100644 index 0000000000..da84e34390 --- /dev/null +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/GridPainter.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph.viewer.renderer; + +import java.awt.Color; +import java.awt.Rectangle; +import java.awt.geom.Point2D; + +import edu.uci.ics.jung.algorithms.layout.Layout; +import edu.uci.ics.jung.visualization.*; +import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator; +import generic.theme.GThemeDefaults.Colors.Palette; +import ghidra.graph.viewer.layout.GridCoordinates; + +/** + * Class for painting the underlying grid used to layout a graph. Used as a visual aid when + * debugging grid based graph layouts. + */ +public class GridPainter { + + private GridCoordinates grid; + + public GridPainter(GridCoordinates gridCoordinates) { + this.grid = gridCoordinates; + } + + public void paintLayoutGridCells(RenderContext renderContext, Layout layout) { + + if (grid == null) { + return; + } + int rowCount = grid.rowCount(); + int colCount = grid.columnCount(); + + GraphicsDecorator g = renderContext.getGraphicsContext(); + Color originalColor = g.getColor(); + Color gridColor = Palette.ORANGE; + Color textColor = Palette.BLACK; + + Rectangle bounds = grid.getBounds(); + int width = bounds.width; + int height = bounds.height; + + MultiLayerTransformer transformer = renderContext.getMultiLayerTransformer(); + int previous = -1; + for (int row = 0; row < rowCount; row++) { + int y = grid.y(row); + if (y == previous) { + continue; // don't paint empty rows + } + previous = y; + Point2D start = new Point2D.Double(0, y); + Point2D end = new Point2D.Double(width, y); + start = transformer.transform(Layer.LAYOUT, start); + end = transformer.transform(Layer.LAYOUT, end); + + g.setColor(textColor); + g.drawString(Integer.toString(row), (float) start.getX() - 20, + (float) (start.getY() + 5)); + + g.setColor(gridColor); + g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY()); + } + + previous = -1; + for (int col = 0; col < colCount; col++) { + int x = grid.x(col); + if (x == previous) { + continue; + } + previous = x; + Point2D start = new Point2D.Double(x, 0); + Point2D end = new Point2D.Double(x, height); + start = transformer.transform(Layer.LAYOUT, start); + end = transformer.transform(Layer.LAYOUT, end); + + g.setColor(textColor); + g.drawString(Integer.toString(col), (float) start.getX() - 5, + (float) (start.getY() - 10)); + + g.setColor(gridColor); + g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY()); + } + + g.setColor(originalColor); + } + +} diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java index 61f844bbb3..c32af97c71 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/renderer/VisualGraphRenderer.java @@ -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. @@ -15,21 +15,15 @@ */ package ghidra.graph.viewer.renderer; -import java.awt.Color; -import java.awt.geom.Point2D; import java.util.*; import com.google.common.base.Function; import edu.uci.ics.jung.algorithms.layout.Layout; -import edu.uci.ics.jung.visualization.*; -import edu.uci.ics.jung.visualization.layout.ObservableCachingLayout; +import edu.uci.ics.jung.visualization.RenderContext; import edu.uci.ics.jung.visualization.renderers.Renderer; -import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator; -import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.graph.viewer.*; import ghidra.graph.viewer.edge.BasicEdgeLabelRenderer; -import ghidra.graph.viewer.layout.*; /** * This was created to add the ability to paint selected vertices above other vertices. We need @@ -42,11 +36,16 @@ import ghidra.graph.viewer.layout.*; public class VisualGraphRenderer> extends edu.uci.ics.jung.visualization.renderers.BasicRenderer { + private static GridPainter gridPainter; + /** - * Used for displaying grid information for graph layouts + * Sets a painter to show an underlying grid. (To see a layout's associated grid, search + * for calls to this method and un-comment them) + * @param gridPainter A painter that paints the grid that a layout was based on. */ - public static Map, LayoutLocationMap> DEBUG_ROW_COL_MAP = - new HashMap<>(); + public static void setGridPainter(GridPainter gridPainter) { + VisualGraphRenderer.gridPainter = gridPainter; + } private Renderer.EdgeLabel edgeLabelRenderer = new BasicEdgeLabelRenderer<>(); @@ -73,6 +72,9 @@ public class VisualGraphRenderer private void mimickSuperPaintingWithoutPaintingSelectedVertices( RenderContext renderContext, Layout layout) { + if (gridPainter != null) { + gridPainter.paintLayoutGridCells(renderContext, layout); + } for (E e : layout.getGraph().getEdges()) { renderEdge(renderContext, layout, e); @@ -94,7 +96,6 @@ public class VisualGraphRenderer // renderEdgeLabel(renderContext, layout, e); // } - paintLayoutGridCells(renderContext, layout); } @Override @@ -124,81 +125,4 @@ public class VisualGraphRenderer edgeLabelRenderer.labelEdge(rc, layout, e, xform.apply(e)); } - @SuppressWarnings({ "unchecked", "rawtypes" }) // the types in the cast matter not - private void paintLayoutGridCells(RenderContext renderContext, Layout layout) { - - // to enable this debug, search java files for commented-out uses of 'DEBUG_ROW_COL_MAP' - Layout key = layout; - if (layout instanceof ObservableCachingLayout) { - key = ((ObservableCachingLayout) layout).getDelegate(); - } - LayoutLocationMap locationMap = DEBUG_ROW_COL_MAP.get(key); - if (locationMap == null) { - return; - } - - int rowCount = locationMap.getRowCount(); - if (rowCount == 0) { - return; // ? - } - - GraphicsDecorator g = renderContext.getGraphicsContext(); - Color originalColor = g.getColor(); - Color gridColor = Palette.ORANGE; - Color textColor = Palette.BLACK; - - boolean isCondensed = locationMap.isCondensed(); - Row lastRow = locationMap.lastRow(); - Column lastColumn = locationMap.lastColumn(); - - if (lastRow == null || lastColumn == null) { - return; // empty graph? - } - - int width = lastColumn.x + lastColumn.getPaddedWidth(isCondensed); - int height = lastRow.y + lastRow.getPaddedHeight(isCondensed); - - MultiLayerTransformer transformer = renderContext.getMultiLayerTransformer(); - for (Row row : locationMap.rows()) { - Point2D start = new Point2D.Double(0, row.y); - start = transformer.transform(Layer.LAYOUT, start); - g.setColor(textColor); - g.drawString(Integer.toString(row.index), (float) start.getX() - 20, - (float) (start.getY() + 5)); - - Point2D end = new Point2D.Double(width, row.y); - end = transformer.transform(Layer.LAYOUT, end); - g.setColor(gridColor); - g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY()); - } - - // close the grid - Point2D start = new Point2D.Double(0, lastRow.y + lastRow.getPaddedHeight(isCondensed)); - start = transformer.transform(Layer.LAYOUT, start); - Point2D end = new Point2D.Double(width, lastRow.y + lastRow.getPaddedHeight(isCondensed)); - end = transformer.transform(Layer.LAYOUT, end); - g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY()); - - for (Column column : locationMap.columns()) { - start = new Point2D.Double(column.x, 0); - start = transformer.transform(Layer.LAYOUT, start); - g.setColor(textColor); - g.drawString(Integer.toString(column.index), (float) start.getX() - 5, - (float) (start.getY() - 10)); - - end = new Point2D.Double(column.x, height); - end = transformer.transform(Layer.LAYOUT, end); - g.setColor(gridColor); - g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY()); - } - - // close the grid - start = new Point2D.Double(lastColumn.x + lastColumn.getPaddedWidth(isCondensed), 0); - start = transformer.transform(Layer.LAYOUT, start); - end = new Point2D.Double(lastColumn.x + lastColumn.getPaddedWidth(isCondensed), height); - end = transformer.transform(Layer.LAYOUT, end); - g.drawLine((int) start.getX(), (int) start.getY(), (int) end.getX(), (int) end.getY()); - - g.setColor(originalColor); - } } diff --git a/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/AbstractVisualGraphTest.java b/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/AbstractVisualGraphTest.java index 11450661b7..20b850add8 100644 --- a/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/AbstractVisualGraphTest.java +++ b/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/AbstractVisualGraphTest.java @@ -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,10 +34,12 @@ import docking.test.AbstractDockingTest; import edu.uci.ics.jung.algorithms.layout.Layout; import ghidra.graph.graphs.AbstractTestVertex; import ghidra.graph.graphs.TestEdge; -import ghidra.graph.support.*; +import ghidra.graph.support.TestLayoutProvider; +import ghidra.graph.support.TestVisualGraph; import ghidra.graph.viewer.event.mouse.VisualGraphMouseTrackingGraphMousePlugin; import ghidra.graph.viewer.event.mouse.VisualGraphPluggableGraphMouse; import ghidra.graph.viewer.event.picking.GPickedState; +import ghidra.graph.viewer.layout.VisualGraphLayout; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -73,7 +75,6 @@ public abstract class AbstractVisualGraphTest extends AbstractDockingTest { protected void buildAndLayoutGraph() throws CancelledException { // the test machine has odd Swing exceptions when we construct UIs off the Swing thread graph = runSwing(() -> buildGraph()); - TestLayoutProvider layoutProvider = createLayoutProvider(); graph.setLayout(layoutProvider.getLayout(graph, TaskMonitor.DUMMY)); graphComponent = runSwing(() -> createGraphComponent(layoutProvider)); @@ -190,7 +191,7 @@ public abstract class AbstractVisualGraphTest extends AbstractDockingTest { GraphViewerUtils.translatePointFromViewSpaceToLayoutSpace(viewPoint, viewer); swing(() -> { - TestGraphLayout layout = graph.getLayout(); + VisualGraphLayout layout = graph.getLayout(); Point2D p = layout.apply(v); layout.setLocation(v, new Point2D.Double(p.getX() + layoutPoint.getX(), p.getY() + layoutPoint.getY())); diff --git a/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/GraphViewerTest.java b/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/GraphViewerTest.java index f268fa5516..0807c2f6b5 100644 --- a/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/GraphViewerTest.java +++ b/Ghidra/Framework/Graph/src/test.slow/java/ghidra/graph/viewer/GraphViewerTest.java @@ -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. @@ -26,12 +26,11 @@ import java.util.List; import org.junit.Test; -import edu.uci.ics.jung.algorithms.layout.KKLayout; -import edu.uci.ics.jung.algorithms.layout.Layout; import generic.test.AbstractGTest; import ghidra.graph.graphs.*; -import ghidra.graph.support.*; +import ghidra.graph.support.TestVertexTooltipProvider; import ghidra.graph.support.TestVertexTooltipProvider.SpyTooltip; +import ghidra.graph.support.TestVisualGraph; public class GraphViewerTest extends AbstractVisualGraphTest { @@ -53,16 +52,6 @@ public class GraphViewerTest extends AbstractVisualGraphTest { return g; } - @Override - protected TestLayoutProvider createLayoutProvider() { - return new TestLayoutProvider() { - @Override - protected Layout createJungLayout(TestVisualGraph g) { - return new KKLayout<>(g); - } - }; - } - @Override protected void initialize() { viewer = graphComponent.getPrimaryViewer(); diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java index f59759fb53..390519d38b 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/AbstractGraphAlgorithmsTest.java @@ -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,10 +77,10 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { protected TestV[] generateSimplyConnectedGraph(int nVertices) { TestV[] vertices = new TestV[nVertices]; for (int i = 0; i < nVertices; i++) { - vertices[i] = vertex(i); + vertices[i] = v(i); } for (int i = 0; i < nVertices - 1; i++) { - edge(vertices[i], vertices[i + 1]); + e(vertices[i], vertices[i + 1]); } return vertices; } @@ -88,12 +88,12 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { protected TestV[] generateCompletelyConnectedGraph(int nVertices) { TestV[] vertices = new TestV[nVertices]; for (int i = 0; i < nVertices; i++) { - vertices[i] = vertex(i); + vertices[i] = v(i); } for (int i = 0; i < nVertices; i++) { for (int j = 0; j < nVertices; j++) { if (i != j) { - edge(vertices[i], vertices[j]); + e(vertices[i], vertices[j]); } } } @@ -103,12 +103,12 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { protected TestV[] generateHalflyConnectedGraph(int nVertices) { TestV[] vertices = new TestV[nVertices]; for (int i = 0; i < nVertices; i++) { - vertices[i] = vertex(i); + vertices[i] = v(i); } // at least one straight line through the graph for (int i = 0; i < nVertices - 1; i++) { - edge(vertices[i], vertices[i + 1]); + e(vertices[i], vertices[i + 1]); } // extra connections @@ -116,7 +116,7 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { for (int i = 0; i < nVertices; i++) { for (int j = 0; j < n; j++) { if (i != j) { - edge(vertices[i], vertices[j]); + e(vertices[i], vertices[j]); } } } @@ -126,12 +126,12 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { protected TestV[] generateHalflyConnectedGraphNoBacktracking(int nVertices) { TestV[] vertices = new TestV[nVertices]; for (int i = 0; i < nVertices; i++) { - vertices[i] = vertex(i); + vertices[i] = v(i); } // at least one straight line through the graph for (int i = 0; i < nVertices - 1; i++) { - edge(vertices[i], vertices[i + 1]); + e(vertices[i], vertices[i + 1]); } // extra connections @@ -139,7 +139,7 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { for (int i = 0; i < nVertices; i++) { for (int j = i; j < n; j++) { if (i != j) { - edge(vertices[i], vertices[j]); + e(vertices[i], vertices[j]); } } } @@ -165,15 +165,15 @@ public abstract class AbstractGraphAlgorithmsTest extends AbstractGenericTest { Assert.fail("Unexpected set size"); } - protected TestV vertex(int id) { + protected TestV v(int id) { return new TestV(id); } - protected TestV vertex(String id) { + protected TestV v(String id) { return new TestV(id); } - protected TestE edge(TestV start, TestV end) { + protected TestE e(TestV start, TestV end) { TestE e = new TestE(start, end); g.addEdge(e); return e; diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java index ee3d809fe6..3c0a495940 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsTest.java @@ -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. @@ -44,9 +44,9 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { @Test public void testGetSources() { - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); g.addVertex(v1); g.addVertex(v2); @@ -55,15 +55,15 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { Set sources = GraphAlgorithms.getSources(g); assertEquals(3, sources.size()); - g.addEdge(edge(v1, v2)); - g.addEdge(edge(v1, v3)); + g.addEdge(e(v1, v2)); + g.addEdge(e(v1, v3)); sources = GraphAlgorithms.getSources(g); assertEquals(1, sources.size()); assertEquals("1", id(sources.iterator().next())); - g.addEdge(edge(v2, v1)); - g.addEdge(edge(v3, v1)); + g.addEdge(e(v2, v1)); + g.addEdge(e(v3, v1)); sources = GraphAlgorithms.getSources(g); assertEquals(0, sources.size()); @@ -75,17 +75,17 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | // v4 -> v5 -> v6 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - edge(v1, v2); - edge(v2, v3); - edge(v1, v4); - edge(v4, v5); - edge(v5, v6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v1, v4); + e(v4, v5); + e(v5, v6); Set descendants = GraphAlgorithms.getDescendants(g, set(v1)); assertContainsExactly(descendants, v1, v2, v3, v4, v5, v6); @@ -105,19 +105,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | | // |->---->----->-----| - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v1, v4); - edge(v4, v5); - edge(v5, v6); - edge(v4, v3); + e(v1, v2); + e(v2, v3); + e(v1, v4); + e(v4, v5); + e(v5, v6); + e(v4, v3); GDirectedGraph subGraph = GraphAlgorithms.createSubGraph(g, set(v1, v2, v3)); Collection vertices = subGraph.getVertices(); @@ -134,22 +134,22 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // V7 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); g.addVertex(v7); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v3, v5); - edge(v5, v6); - edge(v6, v5); - edge(v4, v1); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v3, v5); + e(v5, v6); + e(v6, v5); + e(v4, v1); Set> stronglyConnectedComponents = GraphAlgorithms.getStronglyConnectedComponents(g); @@ -167,20 +167,20 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // v | | // V2 -> V3 -> V5 -> V6 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v3, v5); - edge(v5, v6); - edge(v6, v4); - edge(v4, v1); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v3, v5); + e(v5, v6); + e(v6, v4); + e(v4, v1); Set> stronglyConnectedComponents = GraphAlgorithms.getStronglyConnectedComponents(g); @@ -203,10 +203,10 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { @Test public void testDominance_GetDominators_NoSources() { - TestV v1 = vertex(1); - TestV v2 = vertex(2); - edge(v1, v2); - edge(v2, v1); + TestV v1 = v(1); + TestV v2 = v(2); + e(v1, v2); + e(v2, v1); try { new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -230,15 +230,15 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { | | v4--< */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v1, v4); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v1, v4); ChkDominanceAlgorithm algo = new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -263,15 +263,15 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // v4--< // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v1, v4); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v1, v4); ChkDominanceAlgorithm dominanceAlgorithm = new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -297,15 +297,15 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // v4--< // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v1, v4); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v1, v4); GDirectedGraph> dg = GraphAlgorithms.findDominanceTree(g, TaskMonitor.DUMMY); @@ -324,10 +324,10 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { @Test public void testDominance_GetDominatorGraph_WithDummySource() throws CancelledException { - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); // // 3 sources; 1 sink @@ -335,9 +335,9 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // This will trigger the algorithm to use a unifying dummy node for the source. This used // to lead to a NullPointerException when trying to process the dummy as a dominator. // - edge(v2, v1); - edge(v3, v1); - edge(v4, v1); + e(v2, v1); + e(v3, v1); + e(v4, v1); GDirectedGraph> dg = GraphAlgorithms.findDominanceTree(g, TaskMonitor.DUMMY); @@ -371,33 +371,33 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ //@formatter:on - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); - TestV v11 = vertex(11); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); + TestV v11 = v(11); - edge(v1, v2); - edge(v2, v4); - edge(v4, v3); - edge(v4, v5); + e(v1, v2); + e(v2, v4); + e(v4, v3); + e(v4, v5); - edge(v5, v6); - edge(v5, v7); - edge(v6, v8); - edge(v6, v9); - edge(v7, v9); - edge(v8, v9); - edge(v9, v10); - edge(v9, v11); - edge(v11, v3); - edge(v11, v10); + e(v5, v6); + e(v5, v7); + e(v6, v8); + e(v6, v9); + e(v7, v9); + e(v8, v9); + e(v9, v10); + e(v9, v11); + e(v11, v3); + e(v11, v10); ChkDominanceAlgorithm dominanceAlgorithm = new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -433,19 +433,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v3, v5); - edge(v4, v5); - edge(v5, v6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v3, v5); + e(v4, v5); + e(v5, v6); ChkDominanceAlgorithm algo = new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -476,19 +476,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v3, v5); - edge(v4, v5); - edge(v5, v6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v3, v5); + e(v4, v5); + e(v5, v6); ChkDominanceAlgorithm algo = new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -496,22 +496,22 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // all paths go through v1 //@formatter:off Collection dominated = findDominance(v1, algo); - assertContainsExactly(dominated, edge(v1, v2), - edge(v2, v3), - edge(v2, v4), - edge(v3, v5), - edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(dominated, e(v1, v2), + e(v2, v3), + e(v2, v4), + e(v3, v5), + e(v4, v5), + e(v5, v6)); //@formatter:on // all paths, but v1, are dominated by v2 //@formatter:off dominated = findDominance(v2, algo); - assertContainsExactly(dominated, edge(v2, v3), - edge(v2, v4), - edge(v3, v5), - edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(dominated, e(v2, v3), + e(v2, v4), + e(v3, v5), + e(v4, v5), + e(v5, v6)); //@formatter:on // v3/v4 are two ways through the graph; they do not dominate @@ -521,7 +521,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { assertTrue(dominated.isEmpty()); dominated = findDominance(v5, algo); - assertContainsExactly(dominated, edge(v5, v6)); + assertContainsExactly(dominated, e(v5, v6)); // v6 is the exit; it dominates nothing dominated = findDominance(v6, algo); @@ -540,15 +540,15 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { | | v4--< */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v1, v4); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v1, v4); ChkPostDominanceAlgorithm algo = new ChkPostDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -574,17 +574,17 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ //@formatter:on - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v3, v5); - edge(v5, v2); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v3, v5); + e(v5, v2); ChkPostDominanceAlgorithm algo = new ChkPostDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -615,19 +615,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v3, v5); - edge(v4, v5); - edge(v5, v6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v3, v5); + e(v4, v5); + e(v5, v6); ChkPostDominanceAlgorithm algo = new ChkPostDominanceAlgorithm<>(g, TaskMonitor.DUMMY); @@ -638,7 +638,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { "Found: " + dominated, dominated.isEmpty()); dominated = findPostDominance(v2, algo); - assertContainsExactly(dominated, edge(v1, v2)); + assertContainsExactly(dominated, e(v1, v2)); // v3/v4 are two ways through the graph; they do not post-dominate dominated = findPostDominance(v3, algo); @@ -648,22 +648,22 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { //@formatter:off dominated = findPostDominance(v5, algo); - assertContainsExactly(dominated, edge(v1, v2), - edge(v2, v3), - edge(v2, v4), - edge(v3, v5), - edge(v4, v5)); + assertContainsExactly(dominated, e(v1, v2), + e(v2, v3), + e(v2, v4), + e(v3, v5), + e(v4, v5)); //@formatter:on // all paths go through v6 //@formatter:off dominated = findPostDominance(v6, algo); - assertContainsExactly(dominated, edge(v1, v2), - edge(v2, v3), - edge(v2, v4), - edge(v3, v5), - edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(dominated, e(v1, v2), + e(v2, v3), + e(v2, v4), + e(v3, v5), + e(v4, v5), + e(v5, v6)); //@formatter:on } @@ -687,19 +687,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v3, v5); - edge(v4, v5); - edge(v5, v6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v3, v5); + e(v4, v5); + e(v5, v6); // v1 is the entry; it is not post-dominated by anything Collection dominated = findPostDominance(v1); @@ -707,7 +707,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { "Found: " + dominated, dominated.isEmpty()); dominated = findPostDominance(v2); - assertContainsExactly(dominated, edge(v1, v2)); + assertContainsExactly(dominated, e(v1, v2)); // v3/v4 are two ways through the graph; they do not post-dominate dominated = findPostDominance(v3); @@ -717,22 +717,22 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { //@formatter:off dominated = findPostDominance(v5); - assertContainsExactly(dominated, edge(v1, v2), - edge(v2, v3), - edge(v2, v4), - edge(v3, v5), - edge(v4, v5)); + assertContainsExactly(dominated, e(v1, v2), + e(v2, v3), + e(v2, v4), + e(v3, v5), + e(v4, v5)); //@formatter:on // all paths go through v6 //@formatter:off dominated = findPostDominance(v6); - assertContainsExactly(dominated, edge(v1, v2), - edge(v2, v3), - edge(v2, v4), - edge(v3, v5), - edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(dominated, e(v1, v2), + e(v2, v3), + e(v2, v4), + e(v3, v5), + e(v4, v5), + e(v5, v6)); //@formatter:on } @@ -762,46 +762,46 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ - TestV v1 = vertex(1); // Root - TestV v2 = vertex(2); // Root - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); - TestV v11 = vertex(11); + TestV v1 = v(1); // Root + TestV v2 = v(2); // Root + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); + TestV v11 = v(11); - edge(v1, v3); - edge(v3, v5); - edge(v5, v6); + e(v1, v3); + e(v3, v5); + e(v5, v6); - edge(v2, v4); - edge(v4, v6); + e(v2, v4); + e(v4, v6); - edge(v6, v7); - edge(v7, v8); - edge(v7, v9); - edge(v8, v10); - edge(v9, v10); - edge(v10, v11); + e(v6, v7); + e(v7, v8); + e(v7, v9); + e(v8, v10); + e(v9, v10); + e(v10, v11); ChkDominanceAlgorithm algo = new ChkDominanceAlgorithm<>(g, TaskMonitor.DUMMY); //@formatter:off Collection dominated = findDominance(v1, algo); - assertContainsExactly(dominated, edge(v1, v3), - edge(v3, v5)); + assertContainsExactly(dominated, e(v1, v3), + e(v3, v5)); //@formatter:on dominated = findDominance(v2, algo); - assertContainsExactly(dominated, edge(v2, v4)); + assertContainsExactly(dominated, e(v2, v4)); dominated = findDominance(v3, algo); - assertContainsExactly(dominated, edge(v3, v5)); + assertContainsExactly(dominated, e(v3, v5)); dominated = findDominance(v4, algo); assertTrue(dominated.isEmpty()); @@ -811,19 +811,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { //@formatter:off dominated = findDominance(v6, algo); - assertContainsExactly(dominated, edge(v6, v7), - edge(v7, v8), - edge(v7, v9), - edge(v8, v10), - edge(v9, v10), - edge(v10, v11)); + assertContainsExactly(dominated, e(v6, v7), + e(v7, v8), + e(v7, v9), + e(v8, v10), + e(v9, v10), + e(v10, v11)); dominated = findDominance(v7, algo); - assertContainsExactly(dominated, edge(v7, v8), - edge(v7, v9), - edge(v8, v10), - edge(v9, v10), - edge(v10, v11)); + assertContainsExactly(dominated, e(v7, v8), + e(v7, v9), + e(v8, v10), + e(v9, v10), + e(v10, v11)); //@formatter:on dominated = findDominance(v8, algo); @@ -833,7 +833,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { assertTrue(dominated.isEmpty()); dominated = findDominance(v10, algo); - assertContainsExactly(dominated, edge(v10, v11)); + assertContainsExactly(dominated, e(v10, v11)); } @@ -862,31 +862,31 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ - TestV v1 = vertex(1); // Root - TestV v2 = vertex(2); // Root - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); - TestV v11 = vertex(11); + TestV v1 = v(1); // Root + TestV v2 = v(2); // Root + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); + TestV v11 = v(11); - edge(v1, v3); - edge(v3, v5); - edge(v5, v6); + e(v1, v3); + e(v3, v5); + e(v5, v6); - edge(v2, v4); - edge(v4, v6); + e(v2, v4); + e(v4, v6); - edge(v6, v7); - edge(v7, v8); - edge(v7, v9); - edge(v8, v10); - edge(v9, v10); - edge(v10, v11); + e(v6, v7); + e(v7, v8); + e(v7, v9); + e(v8, v10); + e(v9, v10); + e(v10, v11); Collection dominated = findPostDominance(v1); assertTrue("The start vertex is the root--it should not post dominate any nodes. " + @@ -897,30 +897,30 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { "Found: " + dominated, dominated.isEmpty()); dominated = findPostDominance(v3); - assertContainsExactly(dominated, edge(v1, v3)); + assertContainsExactly(dominated, e(v1, v3)); dominated = findPostDominance(v4); - assertContainsExactly(dominated, edge(v2, v4)); + assertContainsExactly(dominated, e(v2, v4)); //@formatter:off dominated = findPostDominance(v5); - assertContainsExactly(dominated, edge(v1, v3), - edge(v3, v5)); + assertContainsExactly(dominated, e(v1, v3), + e(v3, v5)); dominated = findPostDominance(v6); - assertContainsExactly(dominated, edge(v1, v3), - edge(v3, v5), - edge(v5, v6), - edge(v2, v4), - edge(v4, v6)); + assertContainsExactly(dominated, e(v1, v3), + e(v3, v5), + e(v5, v6), + e(v2, v4), + e(v4, v6)); dominated = findPostDominance(v7); - assertContainsExactly(dominated, edge(v1, v3), - edge(v3, v5), - edge(v5, v6), - edge(v2, v4), - edge(v4, v6), - edge(v6, v7)); + assertContainsExactly(dominated, e(v1, v3), + e(v3, v5), + e(v5, v6), + e(v2, v4), + e(v4, v6), + e(v6, v7)); dominated = findPostDominance(v8); assertTrue(dominated.isEmpty()); @@ -929,29 +929,29 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { assertTrue(dominated.isEmpty()); dominated = findPostDominance(v10); - assertContainsExactly(dominated, edge(v1, v3), - edge(v3, v5), - edge(v5, v6), - edge(v2, v4), - edge(v4, v6), - edge(v6, v7), - edge(v7, v8), - edge(v7, v9), - edge(v8, v10), - edge(v9, v10)); + assertContainsExactly(dominated, e(v1, v3), + e(v3, v5), + e(v5, v6), + e(v2, v4), + e(v4, v6), + e(v6, v7), + e(v7, v8), + e(v7, v9), + e(v8, v10), + e(v9, v10)); dominated = findPostDominance(v11); - assertContainsExactly(dominated, edge(v1, v3), - edge(v3, v5), - edge(v5, v6), - edge(v2, v4), - edge(v4, v6), - edge(v6, v7), - edge(v7, v8), - edge(v7, v9), - edge(v8, v10), - edge(v9, v10), - edge(v10, v11)); + assertContainsExactly(dominated, e(v1, v3), + e(v3, v5), + e(v5, v6), + e(v2, v4), + e(v4, v6), + e(v6, v7), + e(v7, v8), + e(v7, v9), + e(v8, v10), + e(v9, v10), + e(v10, v11)); //@formatter:on } @@ -975,25 +975,25 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); // sink - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); // sink - TestV v8 = vertex(8); - TestV v9 = vertex(9); // sink + TestV v1 = v(1); + TestV v2 = v(2); // sink + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); // sink + TestV v8 = v(8); + TestV v9 = v(9); // sink - edge(v1, v2); - edge(v1, v3); - edge(v3, v4); - edge(v4, v5); - edge(v4, v6); - edge(v4, v7); - edge(v5, v8); - edge(v6, v8); - edge(v8, v9); + e(v1, v2); + e(v1, v3); + e(v3, v4); + e(v4, v5); + e(v4, v6); + e(v4, v7); + e(v5, v8); + e(v6, v8); + e(v8, v9); Collection dominated = findPostDominance(v1); assertTrue("The start vertex is the root--it should not post dominate any nodes. " + @@ -1007,7 +1007,7 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { //@formatter:off dominated = findPostDominance(v4); - assertContainsExactly(dominated, edge(v3, v4)); + assertContainsExactly(dominated, e(v3, v4)); dominated = findPostDominance(v5); assertTrue(dominated.isEmpty()); // there are 3 paths at this level @@ -1019,13 +1019,13 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { assertTrue(dominated.isEmpty()); // there are 3 paths at this level dominated = findPostDominance(v8); - assertContainsExactly(dominated, edge(v5, v8), - edge(v6, v8)); + assertContainsExactly(dominated, e(v5, v8), + e(v6, v8)); dominated = findPostDominance(v9); - assertContainsExactly(dominated, edge(v5, v8), - edge(v6, v8), - edge(v8, v9)); + assertContainsExactly(dominated, e(v5, v8), + e(v6, v8), + e(v8, v9)); //@formatter:on } @@ -1040,8 +1040,8 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // With this change duplicate nodes are acceptable as long as the client correctly // implements equals(). // - edge(vertex(1), vertex(2)); - edge(vertex(1), vertex(3)); + e(v(1), v(2)); + e(v(1), v(3)); GraphAlgorithms.findDominanceTree(g, TaskMonitor.DUMMY); } @@ -1053,19 +1053,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // v v // V2 -> V4 -> V5 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v1, v3); - edge(v2, v6); - edge(v2, v4); - edge(v4, v5); - edge(v3, v4); + e(v1, v2); + e(v1, v3); + e(v2, v6); + e(v2, v4); + e(v4, v5); + e(v3, v4); List postOrder = DepthFirstSorter.postOrder(g); assertEquals(6, postOrder.size()); @@ -1085,19 +1085,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // v v // V2 -> V4 -> V5 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v1, v3); - edge(v2, v6); - edge(v2, v4); - edge(v4, v5); - edge(v3, v4); + e(v1, v2); + e(v1, v3); + e(v2, v6); + e(v2, v4); + e(v4, v5); + e(v3, v4); List preOrder = DepthFirstSorter.preOrder(g); assertEquals(6, preOrder.size()); @@ -1128,19 +1128,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v3, v5); - edge(v4, v5); - edge(v5, v6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v3, v5); + e(v4, v5); + e(v5, v6); List preOrder = DepthFirstSorter.preOrder(g); assertEquals(6, preOrder.size()); @@ -1166,19 +1166,19 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v3, v5); - edge(v4, v5); - edge(v5, v6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v3, v5); + e(v4, v5); + e(v5, v6); List postOrder = DepthFirstSorter.postOrder(g); assertEquals(6, postOrder.size()); @@ -1193,20 +1193,20 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | | // v v // V2 -> V4 -> V5 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - edge(v1, v2); - edge(v1, v3); - edge(v2, v6); - edge(v2, v4); - edge(v4, v5); - edge(v3, v4); - edge(v5, v6); + e(v1, v2); + e(v1, v3); + e(v2, v6); + e(v2, v4); + e(v4, v5); + e(v3, v4); + e(v5, v6); List postOrder = DepthFirstSorter.postOrder(g); assertEquals(6, postOrder.size()); @@ -1220,14 +1220,14 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { @Test public void testDepthFirstPostOrderMultipleSources() { - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); - edge(v1, v3); - edge(v2, v3); - edge(v2, v4); + e(v1, v3); + e(v2, v3); + e(v2, v4); List postOrder = DepthFirstSorter.postOrder(g); assertEquals(4, postOrder.size()); @@ -1247,17 +1247,17 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // v // V4 <-> V5 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v4, v5); - edge(v5, v4); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v4, v5); + e(v5, v4); List postOrder = DepthFirstSorter.postOrder(g); assertEquals(5, postOrder.size()); @@ -1299,23 +1299,23 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); - edge(v1, v2); - edge(v2, v3); - edge(v2, v5); - edge(v3, v4); - edge(v3, v5); - edge(v4, v2); - edge(v5, v7); - edge(v5, v6); - edge(v7, v3); + e(v1, v2); + e(v2, v3); + e(v2, v5); + e(v3, v4); + e(v3, v5); + e(v4, v2); + e(v5, v7); + e(v5, v6); + e(v7, v3); List> circuits = GraphAlgorithms.findCircuits(g, TaskMonitor.DUMMY); assertEquals(3, circuits.size()); @@ -1359,23 +1359,23 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); - edge(v1, v2); - edge(v2, v3); - edge(v2, v5); - edge(v3, v4); - edge(v3, v5); - edge(v4, v2); - edge(v5, v7); - edge(v5, v6); - edge(v7, v3); + e(v1, v2); + e(v2, v3); + e(v2, v5); + e(v3, v4); + e(v3, v5); + e(v4, v2); + e(v5, v7); + e(v5, v6); + e(v7, v3); ListAccumulator> accumulator = new ListAccumulator<>(); GraphAlgorithms.findPaths(g, v2, v4, accumulator, TaskMonitor.DUMMY); @@ -1391,18 +1391,18 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { @Test public void testFindPaths_FullyConnected() throws CancelledException { - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); - edge(v1, v2); - edge(v1, v3); + e(v1, v2); + e(v1, v3); - edge(v2, v3); - edge(v2, v1); + e(v2, v3); + e(v2, v1); - edge(v3, v2); - edge(v3, v1); + e(v3, v2); + e(v3, v1); ListAccumulator> accumulator = new ListAccumulator<>(); GraphAlgorithms.findPaths(g, v1, v2, accumulator, TaskMonitor.DUMMY); @@ -1415,36 +1415,36 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { @Test public void testFindPaths_FullyConnected2() throws CancelledException { - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); - edge(v1, v2); - edge(v1, v3); - edge(v1, v4); - edge(v1, v5); + e(v1, v2); + e(v1, v3); + e(v1, v4); + e(v1, v5); - edge(v2, v1); - edge(v2, v3); - edge(v2, v4); - edge(v2, v5); + e(v2, v1); + e(v2, v3); + e(v2, v4); + e(v2, v5); - edge(v3, v1); - edge(v3, v2); - edge(v3, v4); - edge(v3, v5); + e(v3, v1); + e(v3, v2); + e(v3, v4); + e(v3, v5); - edge(v4, v1); - edge(v4, v2); - edge(v4, v3); - edge(v4, v5); + e(v4, v1); + e(v4, v2); + e(v4, v3); + e(v4, v5); - edge(v5, v1); - edge(v5, v2); - edge(v5, v3); - edge(v5, v4); + e(v5, v1); + e(v5, v2); + e(v5, v3); + e(v5, v4); ListAccumulator> accumulator = new ListAccumulator<>(); GraphAlgorithms.findPaths(g, v1, v5, accumulator, TaskMonitor.DUMMY); @@ -1498,33 +1498,33 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { v1, v3, v6, v7, v9, v10 */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); - edge(v1, v2); - edge(v1, v3); + e(v1, v2); + e(v1, v3); - edge(v2, v10); + e(v2, v10); - edge(v3, v4); - edge(v3, v5); - edge(v3, v6); + e(v3, v4); + e(v3, v5); + e(v3, v6); - edge(v5, v10); + e(v5, v10); - edge(v6, v7); - edge(v7, v8); - edge(v7, v9); + e(v6, v7); + e(v7, v8); + e(v7, v9); - edge(v9, v10); + e(v9, v10); ListAccumulator> accumulator = new ListAccumulator<>(); GraphAlgorithms.findPaths(g, v1, v10, accumulator, TaskMonitor.DUMMY); @@ -1561,34 +1561,34 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { Paths: v1, v3, v5, v10 */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); - edge(v1, v2); - edge(v1, v3); + e(v1, v2); + e(v1, v3); - edge(v2, v1); // back edge + e(v2, v1); // back edge - edge(v3, v4); - edge(v3, v5); - edge(v3, v6); + e(v3, v4); + e(v3, v5); + e(v3, v6); - edge(v5, v10); + e(v5, v10); - edge(v6, v7); + e(v6, v7); - edge(v7, v8); - edge(v7, v9); + e(v7, v8); + e(v7, v9); - edge(v9, v6); // back edge + e(v9, v6); // back edge ListAccumulator> accumulator = new ListAccumulator<>(); @@ -1625,39 +1625,39 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { v1, v3, v6, v7, v9, v10 */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); - TestV v11 = vertex(11); - TestV v12 = vertex(12); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); + TestV v11 = v(11); + TestV v12 = v(12); - edge(v1, v2); - edge(v1, v3); + e(v1, v2); + e(v1, v3); - edge(v2, v10); + e(v2, v10); - edge(v3, v4); - edge(v3, v5); - edge(v3, v6); + e(v3, v4); + e(v3, v5); + e(v3, v6); - edge(v4, v11); + e(v4, v11); - edge(v11, v12); + e(v11, v12); - edge(v5, v10); + e(v5, v10); - edge(v6, v7); - edge(v7, v8); - edge(v7, v9); + e(v6, v7); + e(v7, v8); + e(v7, v9); - edge(v9, v10); + e(v9, v10); ListAccumulator> accumulator = new ListAccumulator<>(); @@ -1696,33 +1696,33 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { v1, v3, v6, v7, v9, v10 */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); - edge(v1, v2); - edge(v1, v3); + e(v1, v2); + e(v1, v3); - edge(v2, v10); + e(v2, v10); - edge(v3, v4); - edge(v3, v5); - edge(v3, v6); + e(v3, v4); + e(v3, v5); + e(v3, v6); - edge(v5, v10); + e(v5, v10); - edge(v6, v7); - edge(v7, v8); - edge(v7, v9); + e(v6, v7); + e(v7, v8); + e(v7, v9); - edge(v9, v10); + e(v9, v10); ListAccumulator> accumulator = new ListAccumulator<>(); @@ -1773,25 +1773,25 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | // v4 -> v5 -> v6 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v4, v5); - edge(v5, v6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v4, v5); + e(v5, v6); //@formatter:off Set edges = GraphAlgorithms.getEdgesFrom(g, v1, true); - assertContainsExactly(edges, edge(v1, v2), - edge(v2, v3), - edge(v2, v4), - edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(edges, e(v1, v2), + e(v2, v3), + e(v2, v4), + e(v4, v5), + e(v5, v6)); //@formatter:on edges = GraphAlgorithms.getEdgesFrom(g, v6, true); @@ -1804,24 +1804,24 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | // v4 -> v5 -> v6 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v4, v5); - edge(v5, v6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v4, v5); + e(v5, v6); //@formatter:off Set edges = GraphAlgorithms.getEdgesFrom(g, v6, false); - assertContainsExactly(edges, edge(v5, v6), - edge(v4, v5), - edge(v2, v4), - edge(v1, v2)); + assertContainsExactly(edges, e(v5, v6), + e(v4, v5), + e(v2, v4), + e(v1, v2)); //@formatter:on edges = GraphAlgorithms.getEdgesFrom(g, v1, false); @@ -1834,31 +1834,31 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | // v4 -> v5 -> v6 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v4, v5); - edge(v5, v6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v4, v5); + e(v5, v6); //@formatter:off Set edges = GraphAlgorithms.getEdgesFrom(g, v5, false); - assertContainsExactly(edges, edge(v4, v5), - edge(v2, v4), - edge(v1, v2)); + assertContainsExactly(edges, e(v4, v5), + e(v2, v4), + e(v1, v2)); edges = GraphAlgorithms.getEdgesFrom(g, v4, false); - assertContainsExactly(edges, edge(v2, v4), - edge(v1, v2)); + assertContainsExactly(edges, e(v2, v4), + e(v1, v2)); edges = GraphAlgorithms.getEdgesFrom(g, v3, false); - assertContainsExactly(edges, edge(v2, v3), - edge(v1, v2)); + assertContainsExactly(edges, e(v2, v3), + e(v1, v2)); //@formatter:on edges = GraphAlgorithms.getEdgesFrom(g, v1, false); @@ -1871,31 +1871,31 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // | // v4 -> v5 -> v6 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - edge(v1, v2); - edge(v2, v3); - edge(v2, v4); - edge(v4, v5); - edge(v5, v6); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v2, v4); + e(v4, v5); + e(v5, v6); //@formatter:off Set edges = GraphAlgorithms.getEdgesFrom(g, v2, true); - assertContainsExactly(edges, edge(v2, v4), - edge(v2, v3), - edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(edges, e(v2, v4), + e(v2, v3), + e(v4, v5), + e(v5, v6)); edges = GraphAlgorithms.getEdgesFrom(g, v4, true); - assertContainsExactly(edges, edge(v4, v5), - edge(v5, v6)); + assertContainsExactly(edges, e(v4, v5), + e(v5, v6)); edges = GraphAlgorithms.getEdgesFrom(g, v5, true); - assertContainsExactly(edges, edge(v5, v6)); + assertContainsExactly(edges, e(v5, v6)); //@formatter:on edges = GraphAlgorithms.getEdgesFrom(g, v3, true); @@ -1922,28 +1922,28 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); - edge(v1, v2); - edge(v2, v3); + e(v1, v2); + e(v2, v3); g.addVertex(v4); - edge(v5, v6); - edge(v6, v7); - edge(v7, v5); + e(v5, v6); + e(v6, v7); + e(v7, v5); - edge(v6, v8); - edge(v8, v9); - edge(v9, v8); + e(v6, v8); + e(v8, v9); + e(v9, v8); Set entries = GraphAlgorithms.getEntryPoints(g); @@ -1965,26 +1965,26 @@ public class GraphAlgorithmsTest extends AbstractGraphAlgorithmsTest { // // V7 - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); g.addVertex(v7); - edge(v1, v2); - edge(v2, v3); - edge(v3, v4); - edge(v3, v5); - edge(v5, v6); - edge(v6, v5); - edge(v4, v1); + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v3, v5); + e(v5, v6); + e(v6, v5); + e(v4, v1); // make an arbitrary root - TestV root = vertex("root"); - g.addEdge(edge(root, v1)); + TestV root = v("root"); + g.addEdge(e(root, v1)); Map depths = GraphAlgorithms.getComplexityDepth(g); assertEquals(g.getVertexCount(), depths.size()); diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java index fda5028e57..d30390f3d8 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphAlgorithmsVisualDebugger.java @@ -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. @@ -88,34 +88,34 @@ public class GraphAlgorithmsVisualDebugger extends AbstractGraphAlgorithmsTest { Paths: v1, v3, v5, v10 */ - TestV v1 = vertex(1); - TestV v2 = vertex(2); - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); - TestV v7 = vertex(7); - TestV v8 = vertex(8); - TestV v9 = vertex(9); - TestV v10 = vertex(10); + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); - edge(v1, v2); - edge(v1, v3); + e(v1, v2); + e(v1, v3); - edge(v2, v1); // back edge + e(v2, v1); // back edge - edge(v3, v4); - edge(v3, v5); - edge(v3, v6); + e(v3, v4); + e(v3, v5); + e(v3, v6); - edge(v5, v10); + e(v5, v10); - edge(v6, v7); + e(v6, v7); - edge(v7, v8); - edge(v7, v9); + e(v7, v8); + e(v7, v9); - edge(v9, v6); // back edge + e(v9, v6); // back edge AlgorithmSteppingTaskMonitor steppingMonitor = new AlgorithmSteppingTaskMonitor(); steppingMonitor = new AlgorithmSelfSteppingTaskMonitor(500); diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphToTreeTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphToTreeTest.java new file mode 100644 index 0000000000..edf57c3d29 --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/GraphToTreeTest.java @@ -0,0 +1,242 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +public class GraphToTreeTest extends AbstractGraphAlgorithmsTest { + private Comparator edgeComparator = (e1, e2) -> e1.toString().compareTo(e2.toString()); + + @Override + protected GDirectedGraph createGraph() { + return GraphFactory.createDirectedGraph(); + } + + @Test + public void testSimpleGraph() { + // v1 -> v2 -> v3 + // | + // v4 -> v5 -> v6 + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v1, v4); + e(v4, v5); + e(v5, v6); + + // expected same graph + // v1 -> v2 -> v3 + // | + // v4 -> v5 -> v6 + + GDirectedGraph tree = GraphAlgorithms.toTree(g, v1, edgeComparator); + assertEdges(tree, e(v1, v2), e(v2, v3), e(v1, v4), e(v4, v5), e(v5, v6)); + } + + @Test + public void testGraphWithLoop() { + // v1 -> v2 -> v3 <---- + // | | + // v4 -> v5 -> v6 | + // | | + // |->---->----->-----| + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + + e(v1, v2); + e(v2, v3); + e(v1, v4); + e(v4, v5); + e(v5, v6); + e(v4, v3); + + // expected: + // v1 -> v2 -> v3 + // | + // v4 -> v5 -> v6 + + GDirectedGraph tree = GraphAlgorithms.toTree(g, v1, edgeComparator); + assertEdges(tree, e(v1, v2), e(v2, v3), e(v1, v4), e(v4, v5), e(v5, v6)); + + } + + @Test + public void testDoubleLoop() { + // V1 <- V4 <-------- + // | ^ ^ + // v | | + // V2 -> V3 -> V5 -> V6 + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v3, v5); + e(v5, v6); + e(v6, v4); + e(v4, v1); + + // expected: + // + // V1 -> V2 -> V3 -> V5 -> V6 -> V4 + + GDirectedGraph tree = GraphAlgorithms.toTree(g, v1, edgeComparator); + assertEdges(tree, e(v1, v2), e(v2, v3), e(v3, v5), e(v5, v6), e(v6, v4)); + } + + @Test + public void testInterleavedBranching() { + + /* + .<-v1->. + | | | + | v4 | + | | | + >--v3 | + | | + v2--< + */ + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + + e(v1, v3); + e(v1, v4); + e(v1, v2); + e(v4, v3); + e(v3, v2); + + // expected: + // v1-> v4 -> v3 -> v2 + + GDirectedGraph tree = GraphAlgorithms.toTree(g, v1, edgeComparator); + assertEdges(tree, e(v1, v4), e(v4, v3), e(v3, v2)); + } + + @Test + public void testMixedGraph() { + + //@formatter:off + /* + v1 + | + v2 + | + sink v3 <- v4 + /\ | + | v5 + | / \ + | v6 v7 + | |\ | + | v8 \ | + | \ | + | \ | + | v9 + | / \ + | / \ + .<--v11-->v10 sink + + */ + //@formatter:on + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); + TestV v11 = v(11); + + e(v1, v2); + e(v2, v4); + e(v4, v3); + e(v4, v5); + + e(v5, v6); + e(v5, v7); + e(v6, v8); + e(v6, v9); + e(v7, v9); + e(v8, v9); + e(v9, v10); + e(v9, v11); + e(v11, v3); + e(v11, v10); + + //@formatter:off + /* expected + + v1 + | + v2 + | + sink v4 + | + v5 + / \ + v6 v7 + | + v8 + | + v9 + | + v11-->v10 + | + v3 + */ + //@formatter:on + + GDirectedGraph tree = GraphAlgorithms.toTree(g, v1, edgeComparator); + assertEdges(tree, e(v1, v2), e(v2, v4), e(v4, v5), e(v5, v6), + e(v5, v7), e(v6, v8), e(v8, v9), e(v9, v11), e(v11, v10), e(v11, v3)); + } + + private void assertEdges(GDirectedGraph tree, TestE... edges) { + Set allEdges = new HashSet<>(tree.getEdges()); + assertEquals("edge count: ", edges.length, allEdges.size()); + for (TestE edge : edges) { + if (!allEdges.contains(edge)) { + fail("Missing expected edge: " + edge); + } + } + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/MutableGDirectedGraphWrapperTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/MutableGDirectedGraphWrapperTest.java index 2aa7e2e134..9615a0091f 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/MutableGDirectedGraphWrapperTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/MutableGDirectedGraphWrapperTest.java @@ -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. @@ -58,25 +58,25 @@ public class MutableGDirectedGraphWrapperTest extends AbstractGraphAlgorithmsTes v6 */ - TestV v1 = vertex(1); // Root - TestV v2 = vertex(2); // Root - TestV v3 = vertex(3); - TestV v4 = vertex(4); - TestV v5 = vertex(5); - TestV v6 = vertex(6); + TestV v1 = v(1); // Root + TestV v2 = v(2); // Root + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); - TestE e1 = edge(v1, v3); - TestE e2 = edge(v3, v5); - TestE e3 = edge(v5, v6); + TestE e1 = e(v1, v3); + TestE e2 = e(v3, v5); + TestE e3 = e(v5, v6); - TestE e4 = edge(v2, v4); - TestE e5 = edge(v4, v6); + TestE e4 = e(v2, v4); + TestE e5 = e(v4, v6); // // Now create the second graph above using a mutable wrapper // MutableGDirectedGraphWrapper wrapper = new MutableGDirectedGraphWrapper<>(g); - TestV fakeRoot = vertex("Fake Root"); + TestV fakeRoot = v("Fake Root"); TestE fakeEdge1 = new TestE(fakeRoot, v1); TestE fakeEdge2 = new TestE(fakeRoot, v2); diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/TopologicalGraphSortTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/TopologicalGraphSortTest.java new file mode 100644 index 0000000000..4f0159030c --- /dev/null +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/TopologicalGraphSortTest.java @@ -0,0 +1,212 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.graph; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.stream.Collectors; + +import org.junit.Test; + +public class TopologicalGraphSortTest extends AbstractGraphAlgorithmsTest { + private Comparator edgeComparator = (e1, e2) -> e1.toString().compareTo(e2.toString()); + + @Override + protected GDirectedGraph createGraph() { + return GraphFactory.createDirectedGraph(); + } + + @Test + public void testSimpleGraph() { + // v1 -> v2 -> v3 + // | + // v4 -> v5 -> v6 + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + e(v1, v2); + e(v2, v3); + e(v1, v4); + e(v4, v5); + e(v5, v6); + + List sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator); + assertList(sorted, v1, v2, v3, v4, v5, v6); + } + + @Test + public void testGraphWithLoop() { + // v1 -> v2 -> v3 <---- + // | | + // v4 -> v5 -> v6 | + // | | + // |->---->----->-----| + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + + e(v1, v2); + e(v2, v3); + e(v1, v4); + e(v4, v5); + e(v5, v6); + e(v4, v3); + + List sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator); + assertList(sorted, v1, v2, v4, v3, v5, v6); + + } + + @Test + public void testDoubleLoop() { + // V1 <- V4 <-------- + // | ^ ^ + // v | | + // V2 -> V3 -> V5 -> V6 + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + + e(v1, v2); + e(v2, v3); + e(v3, v4); + e(v3, v5); + e(v5, v6); + e(v6, v4); + e(v4, v1); + + List sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator); + assertList(sorted, v1, v2, v3, v5, v6, v4); + } + + @Test + public void testInterleavedBranching() { + + /* + .<-v1->. + | | | + | v4 | + | | | + >--v3 | + | | + v2--< + */ + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + + e(v1, v3); + e(v1, v4); + e(v1, v2); + e(v4, v3); + e(v3, v2); + + List sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator); + assertList(sorted, v1, v4, v3, v2); + } + + @Test + public void testMixedGraph() { + + //@formatter:off + /* + v1 + | + v2 + | + sink v3 <- v4 + /\ | + | v5 + | / \ + | v6 v7 + | |\ | + | v8 \ | + | \ | + | \ | + | v9 + | / \ + | / \ + .<--v11-->v10 sink + + */ + //@formatter:on + + TestV v1 = v(1); + TestV v2 = v(2); + TestV v3 = v(3); + TestV v4 = v(4); + TestV v5 = v(5); + TestV v6 = v(6); + TestV v7 = v(7); + TestV v8 = v(8); + TestV v9 = v(9); + TestV v10 = v(10); + TestV v11 = v(11); + + e(v1, v2); + e(v2, v4); + e(v4, v3); + e(v4, v5); + + e(v5, v6); + e(v5, v7); + e(v6, v8); + e(v6, v9); + e(v7, v9); + e(v8, v9); + e(v9, v10); + e(v9, v11); + e(v11, v3); + e(v11, v10); + + List sorted = GraphAlgorithms.topologicalSort(g, v1, edgeComparator); + assertList(sorted, v1, v2, v4, v5, v6, v8, v7, v9, v11, v10, v3); + + } + + private void assertList(List sorted, TestV... vertices) { + if (vertices.length != sorted.size()) { + failed(sorted, vertices); + } + assertEquals(vertices.length, sorted.size()); + for (int i = 0; i < vertices.length; i++) { + if (vertices[i] != sorted.get(i)) { + failed(sorted, vertices); + } + } + } + + private void failed(List sorted, TestV[] vertices) { + String expected = + Arrays.stream(vertices).map(v -> v.toString()).collect(Collectors.joining(", ")); + String actual = sorted.stream().map(v -> v.toString()).collect(Collectors.joining(", ")); + fail("expected: \"" + expected + "\", but got: \"" + actual + "\""); + } +} diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java index 2569ba6226..77e380ff3c 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/algo/viewer/TestGraphAlgorithmSteppingViewerPanel.java @@ -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. @@ -314,13 +314,10 @@ public class TestGraphAlgorithmSteppingViewerPanel> extend GridLocationMap, AlgorithmTestSteppingEdge> grid, int row, int col) { - int existing = grid.row(v); - if (existing > 0) { - return; // already processed + if (grid.containsVertex(v)) { + return; } - - grid.row(v, row); - grid.col(v, col); + grid.set(v, row, col); int nextRow = row++; Collection> children = g.getOutEdges(v); diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/LabelTestVertex.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/LabelTestVertex.java index 451846c00d..7579885665 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/LabelTestVertex.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/LabelTestVertex.java @@ -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. @@ -27,20 +27,23 @@ import generic.theme.GThemeDefaults.Colors.Palette; */ public class LabelTestVertex extends AbstractTestVertex { - private JLabel label = new GDLabel(); + private JLabel label; public LabelTestVertex(String name) { super(name); - label.setText(name); - label.setPreferredSize(new Dimension(50, 50)); - label.setBackground(Palette.GOLD); - label.setOpaque(true); - label.setBorder(BorderFactory.createRaisedBevelBorder()); - label.setHorizontalAlignment(SwingConstants.CENTER); } @Override public JComponent getComponent() { + if (label == null) { + label = new GDLabel(); + label.setText(getName()); + label.setPreferredSize(new Dimension(50, 50)); + label.setBackground(Palette.GOLD); + label.setOpaque(true); + label.setBorder(BorderFactory.createRaisedBevelBorder()); + label.setHorizontalAlignment(SwingConstants.CENTER); + } return label; } diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/TestVertex.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/TestVertex.java index cc855a6684..27d84e82ab 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/TestVertex.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/graphs/TestVertex.java @@ -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. @@ -22,7 +22,7 @@ import javax.swing.JComponent; */ public class TestVertex extends AbstractTestVertex { - protected TestVertex(String name) { + public TestVertex(String name) { super(name); } diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestLayoutProvider.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestLayoutProvider.java index bb6c489c71..73e9596fb0 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestLayoutProvider.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestLayoutProvider.java @@ -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. @@ -22,9 +22,10 @@ import javax.swing.Icon; import edu.uci.ics.jung.algorithms.layout.DAGLayout; import edu.uci.ics.jung.algorithms.layout.Layout; -import ghidra.graph.graphs.TestEdge; import ghidra.graph.graphs.AbstractTestVertex; +import ghidra.graph.graphs.TestEdge; import ghidra.graph.viewer.layout.LayoutProvider; +import ghidra.graph.viewer.layout.VisualGraphLayout; import ghidra.graph.viewer.renderer.ArticulatedEdgeRenderer; import ghidra.graph.viewer.shape.ArticulatedEdgeTransformer; import ghidra.util.exception.CancelledException; @@ -33,7 +34,8 @@ import ghidra.util.task.TaskMonitor; /** * A layout provider used for testing. */ -public class TestLayoutProvider implements LayoutProvider { +public class TestLayoutProvider + implements LayoutProvider { private ArticulatedEdgeTransformer edgeShapeTransformer = new ArticulatedEdgeTransformer<>(); @@ -41,7 +43,8 @@ public class TestLayoutProvider implements LayoutProvider(); @Override - public TestGraphLayout getLayout(TestVisualGraph g, TaskMonitor monitor) + public VisualGraphLayout getLayout(TestVisualGraph g, + TaskMonitor monitor) throws CancelledException { Layout jungLayout = new DAGLayout<>(g); @@ -54,11 +57,6 @@ public class TestLayoutProvider implements LayoutProvider createJungLayout(TestVisualGraph g) { - return new DAGLayout<>(g); - } - @Override public String getLayoutName() { return "Test Layout"; diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java index a64b197566..fef44697a7 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/support/TestVisualGraph.java @@ -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. @@ -18,20 +18,21 @@ package ghidra.graph.support; import java.util.Collection; import ghidra.graph.graphs.*; +import ghidra.graph.viewer.layout.VisualGraphLayout; /** * A visual graph implementation used for testing. */ public class TestVisualGraph extends DefaultVisualGraph { - private TestGraphLayout layout; + private VisualGraphLayout layout; @Override - public TestGraphLayout getLayout() { + public VisualGraphLayout getLayout() { return layout; } - public void setLayout(TestGraphLayout layout) { + public void setLayout(VisualGraphLayout layout) { this.layout = layout; } diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/viewer/layout/GridLocationMapTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/viewer/layout/GridLocationMapTest.java index 4bebefe459..20fe28c0f4 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/graph/viewer/layout/GridLocationMapTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/graph/viewer/layout/GridLocationMapTest.java @@ -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. @@ -15,8 +15,7 @@ */ package ghidra.graph.viewer.layout; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.util.List; @@ -244,6 +243,56 @@ public class GridLocationMapTest { assertCoordinates(v7, 2, 3); } + @Test + public void testGetVertexColumnRanges() { + V v1 = new V("v1"); + V v2 = new V("v2"); + V v3 = new V("v3"); + locations.set(v1, 0, 0); + locations.set(v2, 2, 4); + locations.set(v3, 2, 6); + + GridRange[] columnRanges = locations.getVertexColumnRanges(); + assertEquals(3, columnRanges.length); + assertEquals(new GridRange(0, 0), columnRanges[0]); + assertEquals(new GridRange(), columnRanges[1]); + assertEquals(new GridRange(4, 6), columnRanges[2]); + + } + + @Test + public void testAddGrids() { + // This method creates two grids with points and edge articulations. It then merges + // the other map into the first map with a given row and column shift. It then checks + // the merged grid has all the edge points and vertex points in the expected locations. + V v1 = new V("v1"); + V v2 = new V("v2"); + E e1 = new E(v1, v2); + + V v3 = new V("v3"); + V v4 = new V("v4"); + E e2 = new E(v3, v4); + + locations.set(v1, 0, 0); + locations.set(v2, 2, 4); + locations.setArticulations(e1, List.of(new GridPoint(3, 3))); + + GridLocationMap otherMap = new GridLocationMap<>(); + otherMap.set(v3, 1, 1); + otherMap.set(v4, 2, 2); + otherMap.setArticulations(e1, List.of(new GridPoint(1, 2))); + + locations.add(otherMap, 10, 10); + + assertEquals(13, locations.width()); + assertEquals(13, locations.height()); + + assertCoordinates(v1, 0, 0); + assertCoordinates(v2, 2, 4); + assertCoordinates(v3, 11, 11); + assertCoordinates(v4, 12, 12); + + } //================================================================================================== // Private Methods //================================================================================================== @@ -253,7 +302,7 @@ public class GridLocationMapTest { List> rows = locations.rows(); Row row = getRow(rows, rowIndex); assertEquals("Row " + rowIndex + " has wrong column count", size, - (int) row.getColumnCount()); + row.getColumnCount()); assertEquals(startColumnIndex, (int) row.getStartColumn()); } @@ -279,8 +328,8 @@ public class GridLocationMapTest { } private void assertCoordinates(V v, int row, int col) { - assertEquals("Row not set for '" + v + "'", row, (int) locations.row(v)); - assertEquals("Column not set for '" + v + "'", col, (int) locations.col(v)); + assertEquals("Row not set for '" + v + "'", row, locations.row(v)); + assertEquals("Column not set for '" + v + "'", col, locations.col(v)); } private class V extends TestVertex {