edu.uci.ics.jung.visualization.picking.ShapePickSupport Maven / Gradle / Ivy
/*
* Copyright (c) 2005, The JUNG Authors
*
* All rights reserved.
*
* This software is open-source under the BSD license; see either
* "license.txt" or
* https://github.com/jrtom/jung/blob/master/LICENSE for a description.
* Created on Mar 11, 2005
*
*/
package edu.uci.ics.jung.visualization.picking;
import com.google.common.collect.Sets;
import com.google.common.graph.EndpointPair;
import com.google.common.graph.Network;
import edu.uci.ics.jung.layout.model.LayoutModel;
import edu.uci.ics.jung.layout.model.Point;
import edu.uci.ics.jung.visualization.VisualizationServer;
import edu.uci.ics.jung.visualization.control.TransformSupport;
import edu.uci.ics.jung.visualization.layout.NetworkElementAccessor;
import edu.uci.ics.jung.visualization.spatial.Spatial;
import edu.uci.ics.jung.visualization.spatial.SpatialRTree;
import edu.uci.ics.jung.visualization.spatial.TreeNode;
import edu.uci.ics.jung.visualization.spatial.rtree.LeafNode;
import edu.uci.ics.jung.visualization.spatial.rtree.Node;
import edu.uci.ics.jung.visualization.util.Context;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A NetworkElementAccessor
that returns elements whose Shape
contains the
* specified pick point or region.
*
* @author Tom Nelson
*/
public class ShapePickSupport implements NetworkElementAccessor {
private static final Logger log = LoggerFactory.getLogger(ShapePickSupport.class);
/**
* The available picking heuristics:
*
*
* Style.CENTERED
: returns the element whose center is closest to the pick
* point.
* Style.LOWEST
: returns the first such element encountered. (If the element
* collection has a consistent ordering, this will also be the element "on the bottom", that
* is, the one which is rendered first.)
* Style.HIGHEST
: returns the last such element encountered. (If the element
* collection has a consistent ordering, this will also be the element "on the top", that
* is, the one which is rendered last.)
*
*/
public static enum Style {
LOWEST,
CENTERED,
HIGHEST
};
protected float pickSize;
/**
* The VisualizationServer
in which the this instance is being used for picking. Used
* to retrieve properties such as the layout, renderer, node and edge shapes, and coordinate
* transformations.
*/
protected VisualizationServer vv;
/** The current picking heuristic for this instance. Defaults to CENTERED
. */
protected Style style = Style.CENTERED;
/**
* Creates a ShapePickSupport
for the vv
VisualizationServer, with the
* specified pick footprint and the default pick style. The VisualizationServer
is
* used to access properties of the current visualization (layout, renderer, coordinate
* transformations, node/edge shapes, etc.).
*
* @param vv source of the current Layout
.
* @param pickSize the layoutSize of the pick footprint for line edges
*/
public ShapePickSupport(VisualizationServer vv, float pickSize) {
this.vv = vv;
this.pickSize = pickSize;
}
/**
* Create a ShapePickSupport
for the specified VisualizationServer
with
* a default pick footprint. of layoutSize 2.
*
* @param vv the visualization server used for rendering
*/
public ShapePickSupport(VisualizationServer vv) {
this.vv = vv;
this.pickSize = 2;
}
/**
* Returns the style of picking used by this instance. This specifies which of the elements, among
* those whose shapes contain the pick point, is returned. The available styles are:
*
*
* Style.CENTERED
: returns the element whose center is closest to the pick
* point.
* Style.LOWEST
: returns the first such element encountered. (If the element
* collection has a consistent ordering, this will also be the element "on the bottom", that
* is, the one which is rendered first.)
* Style.HIGHEST
: returns the last such element encountered. (If the element
* collection has a consistent ordering, this will also be the element "on the top", that
* is, the one which is rendered last.)
*
*
* @return the style of picking used by this instance
*/
public Style getStyle() {
return style;
}
/**
* Specifies the style of picking to be used by this instance. This specifies which of the
* elements, among those whose shapes contain the pick point, will be returned. The available
* styles are:
*
*
* Style.CENTERED
: returns the element whose center is closest to the pick
* point.
* Style.LOWEST
: returns the first such element encountered. (If the element
* collection has a consistent ordering, this will also be the element "on the bottom", that
* is, the one which is rendered first.)
* Style.HIGHEST
: returns the last such element encountered. (If the element
* collection has a consistent ordering, this will also be the element "on the top", that
* is, the one which is rendered last.)
*
*
* @param style the style to set
*/
public void setStyle(Style style) {
this.style = style;
}
@Override
public N getNode(LayoutModel layoutModel, Point p) {
return getNode(layoutModel, p.x, p.y);
}
/**
* Returns the node, if any, whose shape contains (x, y). If (x,y) is contained in more than one
* node's shape, returns the node whose center is closest to the pick point.
*
* @param x the x coordinate of the pick point
* @param y the y coordinate of the pick point
* @return the node whose shape contains (x,y), and whose center is closest to the pick point
*/
@Override
public N getNode(LayoutModel layoutModel, double x, double y) {
log.trace("look for node at (layout coords) {},{}", x, y);
TransformSupport transformSupport = vv.getTransformSupport();
N closest = null;
double minDistance = Double.MAX_VALUE;
// x,y is in layout coordinate system.
Point2D pickPoint = new Point2D.Double(x, y);
Spatial nodeSpatial = vv.getNodeSpatial();
if (nodeSpatial.isActive()) {
return getNode(nodeSpatial, layoutModel, pickPoint.getX(), pickPoint.getY());
}
// fall back on checking every node
while (true) {
try {
for (N v : getFilteredNodes()) {
// get the shape for the node (it is at the origin)
Shape shape = vv.getRenderContext().getNodeShapeFunction().apply(v);
// get the node location in layout coordinate system
Point p = layoutModel.apply(v);
if (p == null) {
continue;
}
// translate the shape to the node location in layout coordinates
AffineTransform xform = AffineTransform.getTranslateInstance(p.x, p.y);
shape = xform.createTransformedShape(shape);
if (shape.contains(pickPoint.getX(), pickPoint.getY())) {
if (style == Style.LOWEST) {
// return the first match
return v;
} else if (style == Style.HIGHEST) {
// will return the last match
closest = v;
} else {
// return the node closest to the
// center of a node shape
Rectangle2D bounds = shape.getBounds2D();
double dx = bounds.getCenterX() - pickPoint.getY();
double dy = bounds.getCenterY() - pickPoint.getY();
double dist = dx * dx + dy * dy;
if (dist < minDistance) {
minDistance = dist;
closest = v;
}
}
}
}
break;
} catch (ConcurrentModificationException cme) {
}
}
return closest;
}
/**
* uses the spatialRTree to find the closest node to the points
*
* @param spatial
* @param layoutModel
* @param x in the layout coordinate system
* @param y in the layout coordinate system
* @return the picked node
*/
protected N getNode(Spatial spatial, LayoutModel layoutModel, double x, double y) {
TransformSupport transformSupport = vv.getTransformSupport();
// find the leaf node that would contain a point at x,y
Collection extends TreeNode> containingLeafs =
spatial.getContainingLeafs(new Point2D.Double(x, y));
if (log.isTraceEnabled()) {
log.trace("leaf for {},{} is {}", x, y, containingLeafs);
}
if (containingLeafs == null || containingLeafs.size() == 0) return null;
// make a target circle the same size as the leaf node
// leaf nodes are small when nodes are close and large when they are sparse
// union up all the leafs then make a target
Rectangle2D union = null;
for (TreeNode r : containingLeafs) {
if (union == null) {
union = r.getBounds();
} else {
union = union.createUnion(r.getBounds());
}
}
double width = union.getWidth();
double height = union.getHeight();
double radiusx = width / 2;
double radiusy = height / 2;
Ellipse2D target = new Ellipse2D.Double(x - radiusx, y - radiusy, width, height);
if (log.isTraceEnabled()) {
log.trace("target is {}", target);
}
double minDistance = Double.MAX_VALUE;
// will be the picked node
N closest = null;
// get the all nodes from any leafs that intersect the target
Collection nodes = spatial.getVisibleElements(target);
if (log.isTraceEnabled()) {
log.trace("instead of checking all nodes: {}", getFilteredNodes());
log.trace("out of these candidates: {}...", nodes);
}
// Check the (smaller) set of eligible nodes
// to return the one that contains the (x,y)
for (N node : nodes) {
// get the shape for the node (centered at the origin)
Shape shape = vv.getRenderContext().getNodeShapeFunction().apply(node);
// get the node location
Point p = layoutModel.apply(node);
if (p == null) {
continue;
}
// translate the node shape to its location in layout coordinates
AffineTransform xform = AffineTransform.getTranslateInstance(p.x, p.y);
shape = xform.createTransformedShape(shape);
// translate the pick point from layout coords to screen coords
Point2D layoutPoint = new Point2D.Double(x, y);
log.trace("layout coords of pick point: {}", layoutPoint);
Point2D screenPoint = transformSupport.transform(vv, layoutPoint);
log.trace("screen coords of pick point: {}", screenPoint);
shape = transformSupport.transform(vv, shape);
log.trace("looking in a shape at {} for {}", Node.asString(shape.getBounds2D()), screenPoint);
if (shape.contains(screenPoint)) {
if (style == Style.LOWEST) {
// return the first match
return node;
} else if (style == Style.HIGHEST) {
// will return the last match
closest = node;
} else {
// return the node closest to the
// center of a node shape
Rectangle2D bounds = shape.getBounds2D();
double dx = bounds.getCenterX() - x;
double dy = bounds.getCenterY() - y;
double dist = dx * dx + dy * dy;
if (dist < minDistance) {
minDistance = dist;
closest = node;
}
}
}
}
if (log.isTraceEnabled()) {
log.trace("picked {} with spatial quadtree", closest);
}
return closest;
}
/**
* Returns the nodes whose layout coordinates are contained in Shape
. The shape is in
* screen coordinates, and the graph nodes are transformed to screen coordinates before they are
* tested for inclusion.
*
* @return the Collection
of nodes whose layout
coordinates are
* contained in shape
.
*/
@Override
public Collection getNodes(LayoutModel layoutModel, Shape shape) {
Set pickedNodes = new HashSet<>();
// the pick target shape is in layout coordinate system.
Spatial spatial = vv.getNodeSpatial();
if (spatial != null) {
return getContained(spatial, layoutModel, shape);
}
// fall back on checking every node
while (true) {
try {
for (N v : getFilteredNodes()) {
Point p = layoutModel.apply(v);
if (p == null) {
continue;
}
if (shape.contains(p.x, p.y)) {
pickedNodes.add(v);
}
}
break;
} catch (ConcurrentModificationException cme) {
}
}
return pickedNodes;
}
/**
* use the spatial structure to find nodes inside the passed shape
*
* @param spatial
* @param layoutModel
* @param shape a target shape in layout coordinates
* @return the nodes contained in the target shape
*/
protected Collection getContained(Spatial spatial, LayoutModel layoutModel, Shape shape) {
Collection visible = Sets.newHashSet(spatial.getVisibleElements(shape));
if (log.isTraceEnabled()) {
log.trace("your shape intersects tree cells with these nodes: {}", visible);
}
// some of the nodes that the spatial tree considers visible may be outside
// of the pick target shape. Check this smaller set of nodes and return only
// those that are inside the shape
for (Iterator iterator = visible.iterator(); iterator.hasNext(); ) {
N node = iterator.next();
Point p = layoutModel.apply(node);
if (p == null) {
continue;
}
if (!shape.contains(p.x, p.y)) {
iterator.remove();
}
}
if (log.isTraceEnabled()) {
log.trace("these were actually picked: {}", visible);
}
return visible;
}
/**
* use the spatial R tree to find edges inside the passed shape
*
* @param spatial
* @param layoutModel
* @param shape a target shape in layout coordinates
* @return the nodes contained in the target shape
*/
protected Collection getContained(
SpatialRTree.Edges spatial, LayoutModel layoutModel, Shape shape) {
Collection visible = spatial.getVisibleElements(shape);
if (log.isTraceEnabled()) {
log.trace("your shape intersects tree cells with these nodes: {}", visible);
}
// Network network = spatial.getNetwork();
// some of the nodes that the spatial tree considers visible may be outside
// of the pick target shape. Check this smaller set of nodes and return only
// those that intersect the shape
for (Iterator iterator = visible.iterator(); iterator.hasNext(); ) {
E edge = iterator.next();
Shape edgeShape = getTransformedEdgeShape(edge);
if (!edgeShape.intersects(shape.getBounds())) {
iterator.remove();
}
}
if (log.isTraceEnabled()) {
log.trace("these were actually picked: {}", visible);
}
return visible;
}
/**
* Returns an edge whose shape intersects the 'pickArea' footprint of the passed x,y, coordinates.
*
* @param x the x coordinate of the location (layout coordinate system)
* @param y the y coordinate of the location (layout coordinate system)
* @return an edge whose shape intersects the pick area centered on the location {@code (x,y)}
*/
@Override
public E getEdge(LayoutModel layoutModel, double x, double y) {
// as a Line has no area, we can't always use edgeshape.contains(point) so we
// make a small rectangular pickArea around the point and check if the
// edgeshape.intersects(pickArea)
Rectangle2D pickArea =
new Rectangle2D.Float(
(float) x - pickSize / 2, (float) y - pickSize / 2, pickSize, pickSize);
E closest = null;
double minDistance = Double.MAX_VALUE;
Point2D pickPoint = new Point2D.Double(x, y);
Spatial edgeSpatial = vv.getEdgeSpatial();
if (edgeSpatial != null && edgeSpatial instanceof SpatialRTree.Edges) {
return getEdge(
(SpatialRTree.Edges) edgeSpatial, layoutModel, pickPoint.getX(), pickPoint.getY());
}
while (true) {
try {
// this checks every edge.
for (E e : getFilteredEdges()) {
Shape edgeShape = getTransformedEdgeShape(e);
if (edgeShape == null) {
continue;
}
// because of the transform, the edgeShape is now a GeneralPath
// see if this edge is the closest of any that intersect
if (edgeShape.intersects(pickArea)) {
float cx = 0;
float cy = 0;
float[] f = new float[6];
PathIterator pi = new GeneralPath(edgeShape).getPathIterator(null);
if (pi.isDone() == false) {
pi.next();
pi.currentSegment(f);
cx = f[0];
cy = f[1];
if (pi.isDone() == false) {
pi.currentSegment(f);
cx = f[0];
cy = f[1];
}
}
float dx = (float) (cx - x);
float dy = (float) (cy - y);
float dist = dx * dx + dy * dy;
if (dist < minDistance) {
minDistance = dist;
closest = e;
}
}
}
break;
} catch (ConcurrentModificationException cme) {
}
}
return closest;
}
@Override
public E getEdge(LayoutModel layoutModel, Point2D p) {
return getEdge(layoutModel, p.getX(), p.getY());
}
/**
* uses the spatialRTree to find the closest node to the points
*
* @param spatial
* @param layoutModel
* @param x in the layout coordinate system
* @param y in the layout coordinate system
* @return the picked node
*/
protected E getEdge(
SpatialRTree.Edges spatial, LayoutModel layoutModel, double x, double y) {
// find the leaf nodes that would contain a point at x,y
Collection> containingLeafs = spatial.getContainingLeafs(new Point2D.Double(x, y));
if (log.isTraceEnabled()) {
log.trace("leaf for {},{} is {}", x, y, containingLeafs);
}
if (containingLeafs == null || containingLeafs.size() == 0) return null;
// make a target circle the same size as the leaf node area union
// leaf nodes are small when nodes are close and large when they are sparse
// union up all the leafs then make a target
Rectangle2D union = null;
for (LeafNode r : containingLeafs) {
if (union == null) {
union = r.getBounds();
} else {
union = union.createUnion(r.getBounds());
}
}
double width = union.getWidth();
double height = union.getHeight();
double radiusx = width / 2;
double radiusy = height / 2;
Ellipse2D target = new Ellipse2D.Double(x - radiusx, y - radiusy, width, height);
if (log.isTraceEnabled()) {
log.trace("target is {}", target);
}
double minDistance = Double.MAX_VALUE;
// will be the picked edge
E closest = null;
// get the all nodes from any leafs that intersect the target
Collection edges = spatial.getVisibleElements(target);
if (log.isTraceEnabled()) {
log.trace(
"instead of checking all {} edges: {}", getFilteredEdges().size(), getFilteredEdges());
log.trace("out of these {} candidates: {}...", edges.size(), edges);
}
Rectangle2D pickArea =
new Rectangle2D.Float(
(float) x - pickSize / 2, (float) y - pickSize / 2, pickSize, pickSize);
// Point2D pickPoint = new Point2D.Double(x, y);
// Check the (smaller) set of eligible nodes
// to return the one that contains the (x,y)
for (E edge : edges) {
Shape edgeShape = getTransformedEdgeShape(edge);
if (edgeShape == null) {
continue;
}
// because of the transform, the edgeShape is now a GeneralPath
// see if this edge is the closest of any that intersect
if (edgeShape.intersects(pickArea)) {
float cx = 0;
float cy = 0;
float[] f = new float[6];
PathIterator pi = new GeneralPath(edgeShape).getPathIterator(null);
if (pi.isDone() == false) {
pi.next();
pi.currentSegment(f);
cx = f[0];
cy = f[1];
if (pi.isDone() == false) {
pi.currentSegment(f);
cx = f[0];
cy = f[1];
}
}
float dx = (float) (cx - x);
float dy = (float) (cy - y);
float dist = dx * dx + dy * dy;
if (dist < minDistance) {
minDistance = dist;
closest = edge;
}
}
}
return closest;
}
/**
* Retrieves the shape template for e
and transforms it according to the positions of
* its endpoints in layout
.
*
* @param e the edge whose shape is to be returned
* @return the transformed shape
*/
private Shape getTransformedEdgeShape(E e) {
EndpointPair endpoints = vv.getModel().getNetwork().incidentNodes(e);
N v1 = endpoints.nodeU();
N v2 = endpoints.nodeV();
boolean isLoop = v1.equals(v2);
LayoutModel layoutModel = vv.getModel().getLayoutModel();
Point p1 = layoutModel.apply(v1);
Point p2 = layoutModel.apply(v2);
if (p1 == null || p2 == null) {
return null;
}
float x1 = (float) p1.x;
float y1 = (float) p1.y;
float x2 = (float) p2.x;
float y2 = (float) p2.y;
// translate the edge to the starting node
AffineTransform xform = AffineTransform.getTranslateInstance(x1, y1);
Shape edgeShape =
vv.getRenderContext()
.getEdgeShapeFunction()
.apply(Context.getInstance(vv.getModel().getNetwork(), e));
if (isLoop) {
// make the loops proportional to the layoutSize of the node
Shape s2 = vv.getRenderContext().getNodeShapeFunction().apply(v2);
Rectangle2D s2Bounds = s2.getBounds2D();
xform.scale(s2Bounds.getWidth(), s2Bounds.getHeight());
// move the loop so that the nadir is centered in the node
xform.translate(0, -edgeShape.getBounds2D().getHeight() / 2);
} else {
float dx = x2 - x1;
float dy = y2 - y1;
// rotate the edge to the angle between the nodes
double theta = Math.atan2(dy, dx);
xform.rotate(theta);
// stretch the edge to span the distance between the nodes
float dist = (float) Math.sqrt(dx * dx + dy * dy);
xform.scale(dist, 1.0f);
}
// transform the edge to its location and dimensions
edgeShape = xform.createTransformedShape(edgeShape);
return edgeShape;
}
protected Collection getFilteredNodes() {
Set nodes = vv.getModel().getNetwork().nodes();
return nodesAreFiltered()
? Sets.filter(nodes, vv.getRenderContext().getNodeIncludePredicate()::test)
: nodes;
}
protected Collection getFilteredEdges() {
Set edges = vv.getModel().getNetwork().edges();
return edgesAreFiltered()
? Sets.filter(edges, vv.getRenderContext().getEdgeIncludePredicate()::test)
: edges;
}
/**
* Quick test to allow optimization of getFilteredNodes()
.
*
* @return true
if there is an relaxing node filtering mechanism for this
* visualization, false
otherwise
*/
protected boolean nodesAreFiltered() {
Predicate nodeIncludePredicate = vv.getRenderContext().getNodeIncludePredicate();
return nodeIncludePredicate != null && !nodeIncludePredicate.equals((Predicate) (n -> true));
}
/**
* Quick test to allow optimization of getFilteredEdges()
.
*
* @return true
if there is an relaxing edge filtering mechanism for this
* visualization, false
otherwise
*/
protected boolean edgesAreFiltered() {
Predicate edgeIncludePredicate = vv.getRenderContext().getEdgeIncludePredicate();
return edgeIncludePredicate != null && !edgeIncludePredicate.equals((Predicate) (n -> true));
}
/**
* Returns true
if this node in this graph is included in the collections of elements
* to be rendered, and false
otherwise.
*
* @return true
if this node is included in the collections of elements to be
* rendered, false
otherwise.
*/
protected boolean isNodeRendered(N node) {
Predicate nodeIncludePredicate = vv.getRenderContext().getNodeIncludePredicate();
return nodeIncludePredicate == null || nodeIncludePredicate.test(node);
}
/**
* Returns true
if this edge and its endpoints in this graph are all included in the
* collections of elements to be rendered, and false
otherwise.
*
* @return true
if this edge and its endpoints are all included in the collections of
* elements to be rendered, false
otherwise.
*/
protected boolean isEdgeRendered(E edge) {
Predicate nodeIncludePredicate = vv.getRenderContext().getNodeIncludePredicate();
Predicate edgeIncludePredicate = vv.getRenderContext().getEdgeIncludePredicate();
Network g = vv.getModel().getNetwork();
if (edgeIncludePredicate != null && !edgeIncludePredicate.test(edge)) {
return false;
}
EndpointPair endpoints = g.incidentNodes(edge);
N v1 = endpoints.nodeU();
N v2 = endpoints.nodeV();
return nodeIncludePredicate == null
|| (nodeIncludePredicate.test(v1) && nodeIncludePredicate.test(v2));
}
/**
* Returns the layoutSize of the edge picking area. The picking area is square; the layoutSize is
* specified as the length of one side, in view coordinates.
*
* @return the layoutSize of the edge picking area
*/
public float getPickSize() {
return pickSize;
}
/**
* Sets the layoutSize of the edge picking area.
*
* @param pickSize the length of one side of the (square) picking area, in view coordinates
*/
public void setPickSize(float pickSize) {
this.pickSize = pickSize;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy