mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-10-23 05:31:02 +00:00
GP-768 - Function Graph - condense before edge routing
This commit is contained in:
parent
f2e702d1b2
commit
229c30084e
|
@ -24,6 +24,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.ComponentProvider;
|
||||
import docking.dnd.GenericDataFlavor;
|
||||
|
@ -303,12 +305,9 @@ public class CodeBrowserClipboardProvider extends ByteCopier
|
|||
private Transferable copyAddress() {
|
||||
|
||||
AddressSetView addressSet = getSelectedAddresses();
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
AddressIterator it = addressSet.getAddresses(true);
|
||||
while (it.hasNext()) {
|
||||
buffy.append(it.next()).append('\n');
|
||||
}
|
||||
return createStringTransferable(buffy.toString());
|
||||
String joined = StringUtils.join((Iterator<Address>) it, "\n");
|
||||
return createStringTransferable(joined);
|
||||
}
|
||||
|
||||
protected Transferable copyCode(TaskMonitor monitor) {
|
||||
|
@ -377,8 +376,8 @@ public class CodeBrowserClipboardProvider extends ByteCopier
|
|||
private boolean pasteLabelsComments(Transferable pasteData, boolean pasteLabels,
|
||||
boolean pasteComments) {
|
||||
try {
|
||||
List<?> list = (List<?>) pasteData.getTransferData(
|
||||
CodeUnitInfoTransferable.localDataTypeFlavor);
|
||||
List<?> list =
|
||||
(List<?>) pasteData.getTransferData(CodeUnitInfoTransferable.localDataTypeFlavor);
|
||||
List<CodeUnitInfo> infos = CollectionUtils.asList(list, CodeUnitInfo.class);
|
||||
Command cmd = new CodeUnitInfoPasteCmd(currentLocation.getAddress(), infos, pasteLabels,
|
||||
pasteComments);
|
||||
|
@ -420,7 +419,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier
|
|||
return pasteOperandField((OperandFieldLocation) currentLocation, labelName);
|
||||
}
|
||||
|
||||
// try pasting onto something that is not a label
|
||||
// try pasting onto something that is not a label
|
||||
return maybePasteNonLabelString(labelName);
|
||||
}
|
||||
|
||||
|
@ -451,12 +450,12 @@ public class CodeBrowserClipboardProvider extends ByteCopier
|
|||
String oldName = symbol.getName();
|
||||
Namespace namespace = symbol.getParentNamespace();
|
||||
Address symbolAddress = symbol.getAddress();
|
||||
RenameLabelCmd cmd = new RenameLabelCmd(symbolAddress, oldName, labelName,
|
||||
namespace, SourceType.USER_DEFINED);
|
||||
RenameLabelCmd cmd = new RenameLabelCmd(symbolAddress, oldName, labelName, namespace,
|
||||
SourceType.USER_DEFINED);
|
||||
return tool.execute(cmd, currentProgram);
|
||||
}
|
||||
|
||||
// try pasting onto something that is not a label
|
||||
// try pasting onto something that is not a label
|
||||
return maybePasteNonLabelString(labelName);
|
||||
}
|
||||
|
||||
|
@ -646,7 +645,7 @@ public class CodeBrowserClipboardProvider extends ByteCopier
|
|||
|
||||
//==================================================================================================
|
||||
// Unsupported Operations
|
||||
//==================================================================================================
|
||||
//==================================================================================================
|
||||
|
||||
@Override
|
||||
public void lostOwnership(Transferable transferable) {
|
||||
|
|
|
@ -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.
|
||||
|
@ -38,6 +38,7 @@ import ghidra.app.plugin.core.functiongraph.graph.jung.renderer.DNLArticulatedEd
|
|||
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
|
||||
import ghidra.app.plugin.core.functiongraph.graph.vertex.GroupedFunctionGraphVertex;
|
||||
import ghidra.graph.VisualGraph;
|
||||
import ghidra.graph.viewer.GraphViewerUtils;
|
||||
import ghidra.graph.viewer.layout.*;
|
||||
import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer;
|
||||
import ghidra.program.model.address.Address;
|
||||
|
@ -52,19 +53,19 @@ import ghidra.util.task.TaskMonitor;
|
|||
|
||||
/**
|
||||
* A layout that uses the decompiler to show code nesting based upon conditional logic.
|
||||
*
|
||||
*
|
||||
* <p>Edges returning to the default code flow are painted lighter to de-emphasize them. This
|
||||
* could be made into an option.
|
||||
*
|
||||
* <p>Edge routing herein defaults to 'simple routing'; 'complex routing' is a user option.
|
||||
*
|
||||
* <p>Edge routing herein defaults to 'simple routing'; 'complex routing' is a user option.
|
||||
* Simple routing will reduce edge noise as much as possible by combining/overlapping edges that
|
||||
* flow towards the bottom of the function (returning code flow). Also, edges may fall behind
|
||||
* vertices for some functions. Complex routing allows the user to visually follow the flow
|
||||
* of an individual edge. Complex routing will prevent edges from overlapping and will route
|
||||
* edges around vertices. Simple routing is better when the layout of the vertices is
|
||||
* important to the user; complex routing is better when edges/relationships are more
|
||||
* edges around vertices. Simple routing is better when the layout of the vertices is
|
||||
* important to the user; complex routing is better when edges/relationships are more
|
||||
* important to the user.
|
||||
*
|
||||
*
|
||||
* TODO ideas:
|
||||
* -paint fallthrough differently for all, or just for those returning to the baseline
|
||||
*/
|
||||
|
@ -113,7 +114,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
@Override
|
||||
protected double getCondenseFactor() {
|
||||
// our layout needs more spacing because we have custom edge routing that we want to
|
||||
// our layout needs more spacing because we have custom edge routing that we want to
|
||||
// stand out
|
||||
return .3;
|
||||
}
|
||||
|
@ -201,7 +202,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Special case: fallthrough--don't label this...not sure how to tell fallthrough. For
|
||||
// now assume that any column below or backwards is fallthrough. However,
|
||||
// do label fallthrough if it is the only edge.
|
||||
|
@ -231,10 +232,10 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
Map<FGEdge, List<Point2D>> newEdgeArticulations = new HashMap<>();
|
||||
|
||||
// Condensing Note: we have guilty knowledge that our parent class my condense the
|
||||
// Condensing Note: we have guilty knowledge that our parent class my condense the
|
||||
// vertices and edges towards the center of the graph after we calculate positions.
|
||||
// To prevent the edges from moving to far behind the vertices, we will compensate a
|
||||
// bit for that effect using this offset value. The getEdgeOffset() method below is
|
||||
// bit for that effect using this offset value. The getEdgeOffset() method below is
|
||||
// updated for the condense factor.
|
||||
int edgeOffset = isCondensedLayout()
|
||||
? (int) (VERTEX_TO_EDGE_ARTICULATION_PADDING * (1 - getCondenseFactor()))
|
||||
|
@ -242,7 +243,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
Vertex2dFactory vertex2dFactory =
|
||||
new Vertex2dFactory(transformer, vertexLayoutLocations, layoutToGridMap, edgeOffset);
|
||||
|
||||
//
|
||||
//
|
||||
// Route our edges!
|
||||
//
|
||||
for (FGEdge e : edges) {
|
||||
|
@ -262,11 +263,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
DecompilerBlock loop = block.getParentLoop();
|
||||
|
||||
if (loop != null) {
|
||||
Set<FGVertex> vertices = loop.getVertices();
|
||||
|
||||
Column outermostCol = getOutermostCol(layoutToGridMap, vertices);
|
||||
Column loopEndColumn = layoutToGridMap.nextColumn(outermostCol);
|
||||
List<Point2D> articulations = routeLoopEdge(start, end, loopEndColumn);
|
||||
List<Point2D> articulations =
|
||||
routeUpwardLoop(layoutToGridMap, vertex2dFactory, start, end, loop);
|
||||
newEdgeArticulations.put(e, articulations);
|
||||
continue;
|
||||
}
|
||||
|
@ -275,31 +273,31 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
List<Point2D> articulations = new ArrayList<>();
|
||||
|
||||
//
|
||||
// Basic routing:
|
||||
// Basic routing:
|
||||
// -leave the bottom of the start vertex
|
||||
// -first bend at some constant offset
|
||||
// -move to right or left, to above the end vertex
|
||||
// -second bend above the end vertex at previous constant offset
|
||||
//
|
||||
// Edges start/end on the vertex center. If we offset them to avoid
|
||||
//
|
||||
// Edges start/end on the vertex center. If we offset them to avoid
|
||||
// overlapping, then they produce angles when only using two articulations.
|
||||
// Thus, we create articulations that are behind the vertices to remove
|
||||
// the angles. This points will not be seen.
|
||||
//
|
||||
//
|
||||
//
|
||||
// Complex routing:
|
||||
// -this mode will route edges around vertices
|
||||
//
|
||||
//
|
||||
// One goal for complex edge routing is to prevent overlapping (simple edge routing
|
||||
// prefers overlapping to reduce lines). To prevent overlapping we will use different
|
||||
// offset x and y values, depending upon the start and end vertex row and column
|
||||
// offset x and y values, depending upon the start and end vertex row and column
|
||||
// locations. Specifically, for a given edge direction there will be a bias:
|
||||
// -Edge to the right - leave from the right; arrive to the left
|
||||
// -Edge to the left - leave from the left; arrive to the right
|
||||
// -Edge straight down - go straight down
|
||||
//
|
||||
// For each of the above offsets, there will be an amplifier based upon row/column
|
||||
// distance from start to end vertex. This has the effect that larger vertex
|
||||
// distance from start to end vertex. This has the effect that larger vertex
|
||||
// distances will have a larger offset/spacing.
|
||||
//
|
||||
|
||||
|
@ -331,14 +329,78 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
return newEdgeArticulations;
|
||||
}
|
||||
|
||||
private List<Point2D> routeUpwardLoop(LayoutLocationMap<FGVertex, FGEdge> layoutToGridMap,
|
||||
Vertex2dFactory vertex2dFactory, Vertex2d start, Vertex2d end, DecompilerBlock loop) {
|
||||
Set<FGVertex> loopVertices = loop.getVertices();
|
||||
FGVertex rightmostLoopVertex =
|
||||
getRightmostVertex(layoutToGridMap, vertex2dFactory, loopVertices);
|
||||
|
||||
int startRow = start.rowIndex;
|
||||
int endRow = end.rowIndex;
|
||||
int startColumn = Math.min(start.columnIndex, end.columnIndex);
|
||||
int endColumn = Math.max(start.columnIndex, end.columnIndex);
|
||||
|
||||
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
|
||||
// of the loop bounds. This can happen in a graph when the decompiler uses
|
||||
// goto statements. Use the loop's rightmost vertex to establish the loops
|
||||
// right edge and then use that to check for any stray non-loop vertices.
|
||||
List<Vertex2d> interlopers =
|
||||
getVerticesInBounds(vertex2dFactory, startRow, endRow, startColumn, endColumn);
|
||||
|
||||
// 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);
|
||||
Vertex2d rightmostV2d = vertex2dFactory.get(rightmostVertex);
|
||||
|
||||
// the padding used for these two lines is somewhat arbitrary and may be changed
|
||||
double rightSide = rightmostV2d.getRight() + GraphViewerUtils.EXTRA_LAYOUT_COLUMN_SPACING;
|
||||
double x = Math.min(rightSide,
|
||||
nextColumn.x - GraphViewerUtils.EXTRA_LAYOUT_COLUMN_SPACING_CONDENSED);
|
||||
|
||||
List<Point2D> articulations = routeLoopEdge(start, end, x);
|
||||
return articulations;
|
||||
}
|
||||
|
||||
private List<Vertex2d> getVerticesInBounds(Vertex2dFactory vertex2dFactory, int startRow,
|
||||
int endRow, int startColumn, int endColumn) {
|
||||
|
||||
if (startRow > endRow) { // going upwards
|
||||
int temp = endRow;
|
||||
endRow = startRow;
|
||||
startRow = temp;
|
||||
}
|
||||
|
||||
List<Vertex2d> toCheck = new LinkedList<>();
|
||||
for (int row = startRow; row < endRow + 1; row++) {
|
||||
|
||||
for (int col = startColumn; col < endColumn + 1; col++) {
|
||||
|
||||
// assume any other vertex in our column can clip (it will not clip when
|
||||
// the 'spacing' above pushes the edge away from this column, like for
|
||||
// large row delta values)
|
||||
Vertex2d otherVertex = vertex2dFactory.get(row, col);
|
||||
if (otherVertex != null) {
|
||||
toCheck.add(otherVertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toCheck;
|
||||
}
|
||||
|
||||
private void routeToTheRightGoingUpwards(Vertex2d start, Vertex2d end,
|
||||
Vertex2dFactory vertex2dFactory, List<Point2D> articulations) {
|
||||
|
||||
//
|
||||
// For routing to the right and back up we will leave the start vertex from the right side
|
||||
// For routing to the right and back up we will leave the start vertex from the right side
|
||||
// and enter the end vertex on the right side. As the vertices get further apart, we will
|
||||
// space them further in towards the center.
|
||||
//
|
||||
// space them further in towards the center.
|
||||
//
|
||||
|
||||
int delta = start.rowIndex - end.rowIndex;
|
||||
int multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
|
@ -347,11 +409,9 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
}
|
||||
int distanceSpacing = delta * multiplier;
|
||||
|
||||
// Condensing Note: we have guilty knowledge that our parent class my condense the
|
||||
// vertices and edges towards the center of the graph after we calculate positions.
|
||||
// To prevent the edges from moving to far behind the vertices, we will compensate a
|
||||
// bit for that effect using this offset value. The getEdgeOffset() method is
|
||||
// updated for the condense factor.
|
||||
// Condensing is when the graph will pull nodes closer together on the x axis to
|
||||
// reduce whitespace and make the entire graph easier to see. In this case, update
|
||||
// the offset to avoid running into the moved vertices.
|
||||
int exaggerationFactor = 1;
|
||||
if (isCondensedLayout()) {
|
||||
exaggerationFactor = 2; // determined by trial-and-error; can be made into an option
|
||||
|
@ -369,7 +429,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
y1 = Math.min(y1, startCenterY);
|
||||
articulations.add(new Point2D.Double(x1, y1)); // point is hidden behind the vertex
|
||||
|
||||
// Use the spacing to move the y value towards the top of the vertex. Just like with
|
||||
// Use the spacing to move the y value towards the top of the vertex. Just like with
|
||||
// the x value, restrict the y to the range between the edge and the center.
|
||||
double startRightX = start.getRight();
|
||||
double x2 = startRightX + VERTEX_BORDER_THICKNESS; // start at the end
|
||||
|
@ -434,10 +494,10 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
//
|
||||
// For routing to the left we will leave the start vertex from just left of center and
|
||||
// enter the end vertex on the top, towards the right. As the vertices get further apart,
|
||||
// we will space them further in towards the center of the end vertex. This will keep
|
||||
// enter the end vertex on the top, towards the right. As the vertices get further apart,
|
||||
// we will space them further in towards the center of the end vertex. This will keep
|
||||
// edges with close endpoints from intersecting edges with distant endpoints.
|
||||
//
|
||||
//
|
||||
|
||||
int delta = end.rowIndex - start.rowIndex;
|
||||
int multiplier = EDGE_ENDPOINT_DISTANCE_MULTIPLIER;
|
||||
|
@ -499,7 +559,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
// enter the end vertex on the left side. As the vertices get further apart, we will
|
||||
// space them further in towards the center. This will keep edges with close endpoints
|
||||
// from intersecting edges with distant endpoints.
|
||||
//
|
||||
//
|
||||
|
||||
int delta = end.rowIndex - start.rowIndex;
|
||||
if (delta < 0) {
|
||||
|
@ -529,7 +589,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
double y1 = start.getY();
|
||||
articulations.add(new Point2D.Double(x1, y1)); // point is hidden behind the vertex
|
||||
|
||||
// Use the spacing to move the y value towards the top of the vertex. Just like with
|
||||
// Use the spacing to move the y value towards the top of the vertex. Just like with
|
||||
// the x value, restrict the y to the range between the edge and the center.
|
||||
double x2 = x1;
|
||||
double y2 = end.getTop() + VERTEX_BORDER_THICKNESS;
|
||||
|
@ -556,19 +616,6 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
private void routeAroundColumnVertices(Vertex2d start, Vertex2d end,
|
||||
Vertex2dFactory vertex2dFactory, List<Point2D> articulations, double edgeX) {
|
||||
|
||||
Column column = vertex2dFactory.getColumn(edgeX);
|
||||
int columnIndex = 0;
|
||||
if (column != null) {
|
||||
// a null column happens with a negative x value that is outside of any column
|
||||
columnIndex = column.index;
|
||||
}
|
||||
|
||||
routeAroundColumnVertices(start, end, columnIndex, vertex2dFactory, articulations, edgeX);
|
||||
}
|
||||
|
||||
private void routeAroundColumnVertices(Vertex2d start, Vertex2d end, int column,
|
||||
Vertex2dFactory vertex2dFactory, List<Point2D> articulations, double edgeX) {
|
||||
|
||||
if (useSimpleRouting()) {
|
||||
return;
|
||||
}
|
||||
|
@ -582,14 +629,34 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
startRow = end.rowIndex;
|
||||
}
|
||||
|
||||
int startColumn = Math.min(start.columnIndex, end.columnIndex);
|
||||
int endColumn = Math.max(start.columnIndex, end.columnIndex);
|
||||
if (goingDown) {
|
||||
endRow -= 1;
|
||||
endColumn -= 1;
|
||||
|
||||
if (start.columnIndex <= end.columnIndex) {
|
||||
startRow += 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// going up we swing out to the right; grab the column that is out to the right
|
||||
Column rightColumn = vertex2dFactory.getColumn(edgeX);
|
||||
endColumn = rightColumn.index;
|
||||
}
|
||||
|
||||
List<Vertex2d> toCheck = new LinkedList<>();
|
||||
for (int row = startRow + 1; row < endRow; row++) {
|
||||
// assume any other vertex in our column can clip (it will not clip when
|
||||
// the 'spacing' above pushes the edge away from this column, like for
|
||||
// large row delta values)
|
||||
Vertex2d otherVertex = vertex2dFactory.get(row, column);
|
||||
if (otherVertex != null) {
|
||||
toCheck.add(otherVertex);
|
||||
for (int row = startRow; row < endRow + 1; row++) {
|
||||
|
||||
for (int col = startColumn; col < endColumn + 1; col++) {
|
||||
|
||||
// assume any other vertex in our column can clip (it will not clip when
|
||||
// the 'spacing' above pushes the edge away from this column, like for
|
||||
// large row delta values)
|
||||
Vertex2d otherVertex = vertex2dFactory.get(row, col);
|
||||
if (otherVertex != null) {
|
||||
toCheck.add(otherVertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -605,11 +672,9 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
int padding = VERTEX_TO_EDGE_AVOIDANCE_PADDING;
|
||||
int distanceSpacing = padding + delta; // adding the delta makes overlap less likely
|
||||
|
||||
// Condensing Note: we have guilty knowledge that our parent class my condense the
|
||||
// vertices and edges towards the center of the graph after we calculate positions.
|
||||
// To prevent the edges from moving to far behind the vertices, we will compensate a
|
||||
// bit for that effect using this offset value. The getEdgeOffset() method is
|
||||
// updated for the condense factor.
|
||||
// Condensing is when the graph will pull nodes closer together on the x axis to
|
||||
// reduce whitespace and make the entire graph easier to see. In this case, update
|
||||
// the offset to avoid running into the moved vertices.
|
||||
int vertexToEdgeOffset = otherVertex.getEdgeOffset();
|
||||
int exaggerationFactor = 1;
|
||||
if (isCondensedLayout()) {
|
||||
|
@ -629,20 +694,20 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
// no need to check the 'y' value, as the end vertex is above/below this one
|
||||
if (vertexClipper.isClippingX(otherVertex, edgeX)) {
|
||||
|
||||
/*
|
||||
/*
|
||||
Must route around this vertex - new points:
|
||||
-p1 - just above the intersection point
|
||||
-p2 - just past the left edge
|
||||
-p3 - just past the bottom of the vertex
|
||||
-p4 - back at the original x value
|
||||
|
||||
|
||||
|
|
||||
.___|
|
||||
| .-----.
|
||||
| | |
|
||||
| '-----'
|
||||
'---.
|
||||
|
|
||||
|
|
||||
*/
|
||||
|
||||
// p1 - same x; y just above vertex
|
||||
|
@ -650,19 +715,19 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
double y = vertexClipper.getTopOffset(otherVertex, vertexToEdgeOffset);
|
||||
articulations.add(new Point2D.Double(x, y));
|
||||
|
||||
// Maybe merge points if they are too close together. Visually, many lines
|
||||
// moving around intersecting vertices looks busy. When the intersecting
|
||||
// Maybe merge points if they are too close together. Visually, many lines
|
||||
// moving around intersecting vertices looks busy. When the intersecting
|
||||
// vertices are close together, we remove some of the articulations in order to
|
||||
// smooth out the edges.
|
||||
if (articulations.size() > 2) {
|
||||
|
||||
/*
|
||||
/*
|
||||
The last articulation is the one added before this method was called, which
|
||||
lies just below the intersecting vertex. The articulation before that is
|
||||
the one that is the one that is sending the x value straight into the
|
||||
lies just below the intersecting vertex. The articulation before that is
|
||||
the one that is the one that is sending the x value straight into the
|
||||
intersecting vertex. Delete that point as well so that the entire edge is
|
||||
shifted to the outside of the intersecting vertex. This will get repeated
|
||||
for each vertex that is intersecting.
|
||||
for each vertex that is intersecting.
|
||||
*/
|
||||
Point2D previousArticulation = articulations.get(articulations.size() - 2);
|
||||
int closenessHeight = 50;
|
||||
|
@ -696,15 +761,11 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
return !getLayoutOptions().useEdgeRoutingAroundVertices();
|
||||
}
|
||||
|
||||
private List<Point2D> routeLoopEdge(Vertex2d start, Vertex2d end, Column loopEndColumn) {
|
||||
private List<Point2D> routeLoopEdge(Vertex2d start, Vertex2d end, double x) {
|
||||
|
||||
// going backwards
|
||||
List<Point2D> articulations = new ArrayList<>();
|
||||
|
||||
// loop first point - same y coord as the vertex; x is the middle of the next col
|
||||
int halfWidth = loopEndColumn.getPaddedWidth(isCondensedLayout()) >> 1;
|
||||
double x = loopEndColumn.x + halfWidth; // middle of the column
|
||||
|
||||
int startRow = start.rowIndex;
|
||||
int endRow = end.rowIndex;
|
||||
if (startRow > endRow) { // going upwards
|
||||
|
@ -720,7 +781,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
Point2D first = new Point2D.Double(x, y1);
|
||||
articulations.add(first);
|
||||
|
||||
// loop second point - same y coord as destination;
|
||||
// loop second point - same y coord as destination;
|
||||
// x is the col after the outermost dominated vertex
|
||||
|
||||
Point2D endVertexPoint = end.center;
|
||||
|
@ -739,21 +800,37 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
e.setDefaultAlpha(.25);
|
||||
}
|
||||
|
||||
private Column getOutermostCol(LayoutLocationMap<FGVertex, FGEdge> layoutLocations,
|
||||
Set<FGVertex> vertices) {
|
||||
private FGVertex getRightmostVertex(LayoutLocationMap<FGVertex, FGEdge> layoutLocations,
|
||||
Vertex2dFactory vertex2dFactory, Set<FGVertex> vertices) {
|
||||
|
||||
Column outermost = null;
|
||||
List<Vertex2d> points = new ArrayList<>();
|
||||
for (FGVertex v : vertices) {
|
||||
Column col = layoutLocations.col(v);
|
||||
if (outermost == null) {
|
||||
outermost = col;
|
||||
Vertex2d v2d = vertex2dFactory.get(v);
|
||||
points.add(v2d);
|
||||
}
|
||||
|
||||
FGVertex v = getRightmostVertex(points);
|
||||
return v;
|
||||
}
|
||||
|
||||
private FGVertex getRightmostVertex(Collection<Vertex2d> points) {
|
||||
|
||||
Vertex2d rightmost = null;
|
||||
for (Vertex2d v2d : points) {
|
||||
if (rightmost == null) {
|
||||
rightmost = v2d;
|
||||
}
|
||||
else if (col.x > outermost.x) {
|
||||
outermost = col;
|
||||
else {
|
||||
// the rightmost is that which extends furthest to the right
|
||||
double current = rightmost.getRight();
|
||||
double other = v2d.getRight();
|
||||
if (other > current) {
|
||||
rightmost = v2d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outermost;
|
||||
return rightmost.v;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -840,8 +917,11 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
BlockCopy copy = (BlockCopy) child;
|
||||
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
buffy.append(printDepth(depth, depth + 1)).append(' ').append(ID).append(
|
||||
" plain - ").append(copy.getRef());
|
||||
buffy.append(printDepth(depth, depth + 1))
|
||||
.append(' ')
|
||||
.append(ID)
|
||||
.append(" plain - ")
|
||||
.append(copy.getRef());
|
||||
|
||||
debug(buffy.toString());
|
||||
}
|
||||
|
@ -958,7 +1038,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
//==================================================================================================
|
||||
|
||||
/**
|
||||
* Encapsulates knowledge of edge direction (up/down, left/right) and uses that knowledge
|
||||
|
@ -1060,7 +1140,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
}
|
||||
|
||||
/**
|
||||
* A class that represents 2D information about the contained vertex, such as location,
|
||||
* A class that represents 2D information about the contained vertex, such as location,
|
||||
* bounds, row and column of the layout grid.
|
||||
*/
|
||||
private class Vertex2d {
|
||||
|
@ -1207,8 +1287,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
int row = startRow;
|
||||
|
||||
for (int i = 0; i < allChildren.size(); i++) {
|
||||
DecompilerBlock block = allChildren.get(i);
|
||||
for (DecompilerBlock block : allChildren) {
|
||||
if (block instanceof DecompilerBlockGraph) {
|
||||
row = ((DecompilerBlockGraph) block).setRows(row);
|
||||
}
|
||||
|
@ -1229,8 +1308,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
String getChildrenString(int depth) {
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
int childCount = 0;
|
||||
for (int i = 0; i < allChildren.size(); i++) {
|
||||
DecompilerBlock block = allChildren.get(i);
|
||||
for (DecompilerBlock block : allChildren) {
|
||||
if (block instanceof DecompilerBlockGraph) {
|
||||
|
||||
String blockName = block.getName();
|
||||
|
@ -1315,8 +1393,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
@Override
|
||||
DecompilerBlock getBlock(FGVertex vertex) {
|
||||
//
|
||||
// Note: we currently allow grouping in this layout. When we search for a vertex,
|
||||
// we have to check each vertex inside of the given group *and* each vertex
|
||||
// Note: we currently allow grouping in this layout. When we search for a vertex,
|
||||
// we have to check each vertex inside of the given group *and* each vertex
|
||||
// inside of the vertex that belongs to this decompiler block.
|
||||
//
|
||||
if (vertex instanceof GroupedFunctionGraphVertex) {
|
||||
|
@ -1447,9 +1525,8 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
// The 'list' structure for children's nesting:
|
||||
// -all nodes are at the same level
|
||||
//
|
||||
for (int i = 0; i < allChildren.size(); i++) {
|
||||
for (DecompilerBlock block : allChildren) {
|
||||
int column = col;
|
||||
DecompilerBlock block = allChildren.get(i);
|
||||
block.setCol(column);
|
||||
}
|
||||
|
||||
|
@ -1477,8 +1554,7 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
// -each successive condition is another level nested
|
||||
//
|
||||
int column = col;
|
||||
for (int i = 0; i < allChildren.size(); i++) {
|
||||
DecompilerBlock block = allChildren.get(i);
|
||||
for (DecompilerBlock block : allChildren) {
|
||||
block.setCol(column);
|
||||
column++;
|
||||
}
|
||||
|
@ -1515,11 +1591,10 @@ public class DecompilerNestedLayout extends AbstractFGLayout {
|
|||
|
||||
//
|
||||
// The 'do' structure for children's nesting:
|
||||
// -all blocks nested
|
||||
// -all blocks nested
|
||||
//
|
||||
int column = col + 1;
|
||||
for (int i = 0; i < allChildren.size(); i++) {
|
||||
DecompilerBlock block = allChildren.get(i);
|
||||
for (DecompilerBlock block : allChildren) {
|
||||
block.setCol(column);
|
||||
}
|
||||
|
||||
|
|
|
@ -44,36 +44,36 @@ import ghidra.util.task.TaskMonitor;
|
|||
* A base layout that marries the Visual Graph and Jung layout interfaces. This class allows
|
||||
* you to create new layouts while stubbing the Jung layout methods.
|
||||
*
|
||||
* <P>This class essentially takes in client-produced grid row and column indices and
|
||||
* <P>This class essentially takes in client-produced grid row and column indices and
|
||||
* produces layout locations for those values.
|
||||
*
|
||||
* <P>This an implementation the Jung {@link Layout} interface that handles most of the
|
||||
* <P>This an implementation the Jung {@link Layout} interface that handles most of the
|
||||
* layout implementation for you. Things to know:
|
||||
* <UL>
|
||||
* <LI>You should call initialize() inside of your constructor</LI>
|
||||
* <LI>You must implement {@link #performInitialGridLayout(VisualGraph)} - this is where
|
||||
* <LI>You must implement {@link #performInitialGridLayout(VisualGraph)} - this is where
|
||||
* you align your vertices (and optionally edge articulations) on a grid. This grid
|
||||
* will be translated into layout space points for you.</LI>
|
||||
* <LI>If you wish to use articulation points in your edges, you must override
|
||||
* {@link #usesEdgeArticulations()} to return true.</LI>
|
||||
* <LI>If you wish to use articulation points in your edges, you must override
|
||||
* {@link #usesEdgeArticulations()} to return true.</LI>
|
||||
* </UL>
|
||||
*
|
||||
*
|
||||
* <p><a id="column_centering"></A>By default, this class will create x-position values that
|
||||
* are aligned with the column's x-position. You can override
|
||||
* are aligned with the column's x-position. You can override
|
||||
* {@link #getVertexLocation(VisualVertex, Column, Row, Rectangle)} in order to center the
|
||||
* vertex within its column
|
||||
* {@link #getCenteredVertexLocation(VisualVertex, Column, Row, Rectangle)}. Also note though
|
||||
* {@link #getCenteredVertexLocation(VisualVertex, Column, Row, Rectangle)}. Also note though
|
||||
* that if your layout returns true for {@link #isCondensedLayout()},
|
||||
* then the centering will be condensed and slightly off.
|
||||
*
|
||||
* then the centering will be condensed and slightly off.
|
||||
*
|
||||
* @param <V> the vertex type
|
||||
* @param <E> the edge type
|
||||
*
|
||||
*
|
||||
* @see GridLocationMap
|
||||
* @see LayoutPositions
|
||||
*/
|
||||
//@formatter:off
|
||||
public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
||||
public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
||||
E extends VisualEdge<V>>
|
||||
extends AbstractLayout<V, E>
|
||||
implements VisualGraphLayout<V, E> {
|
||||
|
@ -106,9 +106,9 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
|
||||
/**
|
||||
* This is the method that is called to perform the actual layout. While this method is
|
||||
* running, the {@link #monitor} variable has been set so that you can call
|
||||
* running, the {@link #monitor} variable has been set so that you can call
|
||||
* {@link TaskMonitor#checkCanceled()}.
|
||||
*
|
||||
*
|
||||
* @param g the graph
|
||||
* @return the new grid location
|
||||
* @throws CancelledException if the operation was cancelled
|
||||
|
@ -151,10 +151,10 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns true if this layout is in a condensed mode, which means to reduce space
|
||||
* between vertices and edges. This is useful to save space. Subclasses may choose to
|
||||
* Returns true if this layout is in a condensed mode, which means to reduce space
|
||||
* between vertices and edges. This is useful to save space. Subclasses may choose to
|
||||
* have this setting controlled via an option that the user can toggle.
|
||||
*
|
||||
*
|
||||
* @return true for a condensed layout
|
||||
*/
|
||||
protected boolean isCondensedLayout() {
|
||||
|
@ -218,11 +218,11 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
}
|
||||
|
||||
/**
|
||||
* This class has implemented {@link #cloneLayout(VisualGraph)} in order to properly
|
||||
* This class has implemented {@link #cloneLayout(VisualGraph)} in order to properly
|
||||
* initialize location information in the layout so that subclasses do not have to. Each
|
||||
* subclass still needs to create the new instance of the layout that is being cloned, as
|
||||
* this class does not know how to do so.
|
||||
*
|
||||
*
|
||||
* @param newGraph the new graph for the new layout
|
||||
* @return the new layout
|
||||
*/
|
||||
|
@ -237,7 +237,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
|
||||
/**
|
||||
* Takes the given layout and copies the layout information this layout into that layout
|
||||
*
|
||||
*
|
||||
* @param newLayout the new layout to update
|
||||
*/
|
||||
protected void initializeClonedLayout(AbstractVisualGraphLayout<V, E> newLayout) {
|
||||
|
@ -260,7 +260,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
|
||||
List<Point2D> bends = edgesToBends.get(e);
|
||||
if (bends == null) {
|
||||
// New edge is not in the old graph. This can happen if the old graph has
|
||||
// New edge is not in the old graph. This can happen if the old graph has
|
||||
// grouped vertices and some edges have been removed.
|
||||
continue;
|
||||
}
|
||||
|
@ -313,14 +313,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
Map<V, Point2D> vertexLayoutLocations =
|
||||
positionVerticesInLayoutSpace(transformer, vertices, layoutLocations);
|
||||
|
||||
Map<E, List<Point2D>> edgeLayoutArticulationLocations =
|
||||
positionEdgeArticulationsInLayoutSpace(transformer, vertexLayoutLocations, edges,
|
||||
layoutLocations);
|
||||
|
||||
// DEGUG triggers grid lines to be printed; useful for debugging
|
||||
// VisualGraphRenderer.DEBUG_ROW_COL_MAP.put((Graph<?, ?>) visualGraph,
|
||||
// layoutLocations.copy());
|
||||
|
||||
Map<E, List<Point2D>> edgeLayoutArticulationLocations = new HashMap<>();
|
||||
Rectangle graphBounds =
|
||||
getTotalGraphSize(vertexLayoutLocations, edgeLayoutArticulationLocations, transformer);
|
||||
double centerX = graphBounds.getCenterX();
|
||||
|
@ -332,6 +325,12 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
centerX, centerY);
|
||||
}
|
||||
|
||||
edgeLayoutArticulationLocations = positionEdgeArticulationsInLayoutSpace(transformer,
|
||||
vertexLayoutLocations, edges, layoutLocations);
|
||||
|
||||
// DEGUG triggers grid lines to be printed; useful for debugging
|
||||
// VisualGraphRenderer.DEBUG_ROW_COL_MAP.put(this, layoutLocations.copy());
|
||||
|
||||
layoutLocations.dispose();
|
||||
gridLocations.dispose();
|
||||
|
||||
|
@ -368,7 +367,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
|
||||
/**
|
||||
* Returns a location for the given vertex that is centered within its cell
|
||||
*
|
||||
*
|
||||
* @param v the vertex
|
||||
* @param col the vertex's column in the grid
|
||||
* @param row the vertex's row in the grid
|
||||
|
@ -379,7 +378,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
//
|
||||
// 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
|
||||
// them will not be straight *when the vertices are painted off-center on their column*
|
||||
// them will not be straight *when the vertices are painted off-center on their column*
|
||||
// (which means they are left-aligned). By centering the vertex, the center points of
|
||||
// the differently sized vertices (on the same column and different rows) will be aligned.
|
||||
//
|
||||
|
@ -423,7 +422,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
//
|
||||
// half-height offsets the articulation points, which keeps long edge lines from
|
||||
// overlapping as much
|
||||
//
|
||||
//
|
||||
boolean isCondensed = isCondensedLayout();
|
||||
int x = col.x + (col.getPaddedWidth(isCondensed) >> 1);
|
||||
int y = row.y + (row.getPaddedHeight(isCondensed) >> 1);
|
||||
|
@ -464,8 +463,8 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
VisualGraphVertexShapeTransformer<V> transformer, double centerX, double centerY) {
|
||||
|
||||
//
|
||||
// Note: we move the articulations and vertices closer together on the x-axis. We do
|
||||
// not move the y-axis, as that is already as close together as we would like at
|
||||
// Note: we move the articulations and vertices closer together on the x-axis. We do
|
||||
// not move the y-axis, as that is already as close together as we would like at
|
||||
// this point.
|
||||
//
|
||||
double condenseFactor = getCondenseFactor();
|
||||
|
@ -496,14 +495,14 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
}
|
||||
|
||||
//
|
||||
// The above aggressive condensing may lead to neighboring node overlapping for
|
||||
// nodes in the same row. Check to see if we need to move the nodes to avoid this case.
|
||||
// The above aggressive condensing may lead to neighboring node overlapping for
|
||||
// nodes in the same row. Check to see if we need to move the nodes to avoid this case.
|
||||
//
|
||||
unclip(rows, newLocations, transformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount (from 0 to 1.0) by which to condense the vertices of the graph when that
|
||||
/**
|
||||
* The amount (from 0 to 1.0) by which to condense the vertices of the graph when that
|
||||
* feature is enabled. The default is .5 (50%). A value of 1.0 would be fully-condensed
|
||||
* such that all vertices are aligned on the x-axis on the center of the graph.
|
||||
* @return the condense factor
|
||||
|
@ -601,7 +600,7 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
//
|
||||
// Visual points (after the centering has taken place). Update the location to account
|
||||
// for this centering before checking for clipping.
|
||||
//
|
||||
//
|
||||
int myWidth = vertexBounds.width >> 1; // half width
|
||||
int myHeight = vertexBounds.height >> 1; // half height
|
||||
double x = vertexPoint.getX();
|
||||
|
@ -635,20 +634,20 @@ public abstract class AbstractVisualGraphLayout<V extends VisualVertex,
|
|||
double newX = otherX + offset;
|
||||
vertexPoint.setLocation(newX, oldY); // editing this point changes the map's value
|
||||
|
||||
// DEBUG this can be deleted in the future, future, future
|
||||
// DEBUG this can be deleted in the future, future, future
|
||||
// //@formatter:off
|
||||
// Msg.debug(this,
|
||||
// vertex +
|
||||
// vertex +
|
||||
// "\n\tat " + vertexPoint.getX() +
|
||||
// "\n\tvisual x: " + myNewPoint +
|
||||
// "\n\tw: " + vertexBounds.width +
|
||||
// "\n\t\t" + otherVertex +
|
||||
// "\n\tw: " + vertexBounds.width +
|
||||
// "\n\t\t" + otherVertex +
|
||||
// "\n\t\tat: " + otherVertexPoint +
|
||||
// "\n\t\tvisual x: " + otherNewPoint.getX() +
|
||||
// "\n\t\tw: " + otherVertexBounds.width +
|
||||
// "\n\tclip: " + intersection.width +
|
||||
// "\n\toffset: " + offset +
|
||||
// "\n\tnew pt: " + newX);
|
||||
// "\n\t\tw: " + otherVertexBounds.width +
|
||||
// "\n\tclip: " + intersection.width +
|
||||
// "\n\toffset: " + offset +
|
||||
// "\n\tnew pt: " + newX);
|
||||
// //@formatter:on
|
||||
|
||||
}
|
||||
|
|
|
@ -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,8 +22,8 @@ import java.util.*;
|
|||
import com.google.common.base.Function;
|
||||
|
||||
import edu.uci.ics.jung.algorithms.layout.Layout;
|
||||
import edu.uci.ics.jung.graph.Graph;
|
||||
import edu.uci.ics.jung.visualization.*;
|
||||
import edu.uci.ics.jung.visualization.layout.ObservableCachingLayout;
|
||||
import edu.uci.ics.jung.visualization.renderers.Renderer;
|
||||
import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
|
||||
import ghidra.graph.viewer.*;
|
||||
|
@ -34,9 +34,9 @@ import ghidra.graph.viewer.layout.*;
|
|||
* This was created to add the ability to paint selected vertices above other vertices. We need
|
||||
* this since the Jung Graph has no notion of Z-order and thus does not let us specify that any
|
||||
* particular vertex should be above another one.
|
||||
*
|
||||
*
|
||||
* @param <V> the vertex type
|
||||
* @param <E> the edge type
|
||||
* @param <E> the edge type
|
||||
*/
|
||||
public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>>
|
||||
extends edu.uci.ics.jung.visualization.renderers.BasicRenderer<V, E> {
|
||||
|
@ -44,7 +44,8 @@ public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>
|
|||
/**
|
||||
* Used for displaying grid information for graph layouts
|
||||
*/
|
||||
public static Map<Graph<?, ?>, LayoutLocationMap<?, ?>> DEBUG_ROW_COL_MAP = new HashMap<>();
|
||||
public static Map<VisualGraphLayout<?, ?>, LayoutLocationMap<?, ?>> DEBUG_ROW_COL_MAP =
|
||||
new HashMap<>();
|
||||
|
||||
private Renderer.EdgeLabel<V, E> edgeLabelRenderer = new BasicEdgeLabelRenderer<>();
|
||||
|
||||
|
@ -122,11 +123,15 @@ public class VisualGraphRenderer<V extends VisualVertex, E extends VisualEdge<V>
|
|||
edgeLabelRenderer.labelEdge(rc, layout, e, xform.apply(e));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) // the types in the cast matter not
|
||||
private void paintLayoutGridCells(RenderContext<V, E> renderContext, Layout<V, E> layout) {
|
||||
|
||||
// to enable this debug, search java files for commented-out uses of 'DEBUG_ROW_COL_MAP'
|
||||
Graph<V, E> graph = layout.getGraph();
|
||||
LayoutLocationMap<?, ?> locationMap = DEBUG_ROW_COL_MAP.get(graph);
|
||||
Layout<V, E> key = layout;
|
||||
if (layout instanceof ObservableCachingLayout) {
|
||||
key = ((ObservableCachingLayout) layout).getDelegate();
|
||||
}
|
||||
LayoutLocationMap<?, ?> locationMap = DEBUG_ROW_COL_MAP.get(key);
|
||||
if (locationMap == null) {
|
||||
return;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user