View Javadoc
1   /*
2    * Copyright (c) 2005, The JUNG Authors 
3    *
4    * All rights reserved.
5    *
6    * This software is open-source under the BSD license; see either
7    * "license.txt" or
8    * https://github.com/jrtom/jung/blob/master/LICENSE for a description.
9    * Created on Mar 11, 2005
10   *
11   */
12  package edu.uci.ics.jung.visualization.picking;
13  
14  import java.awt.Shape;
15  import java.awt.geom.AffineTransform;
16  import java.awt.geom.GeneralPath;
17  import java.awt.geom.PathIterator;
18  import java.awt.geom.Point2D;
19  import java.awt.geom.Rectangle2D;
20  import java.util.Collection;
21  import java.util.ConcurrentModificationException;
22  import java.util.HashSet;
23  import java.util.LinkedHashSet;
24  import java.util.Set;
25  
26  import com.google.common.base.Predicate;
27  import com.google.common.base.Predicates;
28  
29  import edu.uci.ics.jung.algorithms.layout.GraphElementAccessor;
30  import edu.uci.ics.jung.algorithms.layout.Layout;
31  import edu.uci.ics.jung.graph.Graph;
32  import edu.uci.ics.jung.graph.util.Context;
33  import edu.uci.ics.jung.graph.util.Pair;
34  import edu.uci.ics.jung.visualization.Layer;
35  import edu.uci.ics.jung.visualization.VisualizationServer;
36  
37  /**
38   * A <code>GraphElementAccessor</code> that returns elements whose <code>Shape</code>
39   * contains the specified pick point or region.
40   * 
41   * @author Tom Nelson
42   *
43   */
44  public class ShapePickSupport<V, E> implements GraphElementAccessor<V,E> {
45  
46  	/**
47  	 * The available picking heuristics:
48       * <ul>
49       * <li><code>Style.CENTERED</code>: returns the element whose 
50       * center is closest to the pick point.
51       * <li><code>Style.LOWEST</code>: returns the first such element
52       * encountered.  (If the element collection has a consistent
53       * ordering, this will also be the element "on the bottom", 
54       * that is, the one which is rendered first.) 
55       * <li><code>Style.HIGHEST</code>: returns the last such element
56       * encountered.  (If the element collection has a consistent
57       * ordering, this will also be the element "on the top", 
58       * that is, the one which is rendered last.)
59       * </ul>
60  	 *
61  	 */
62  	public static enum Style { LOWEST, CENTERED, HIGHEST };
63  
64      protected float pickSize;
65      
66      /**
67       * The <code>VisualizationServer</code> in which the 
68       * this instance is being used for picking.  Used to 
69       * retrieve properties such as the layout, renderer,
70       * vertex and edge shapes, and coordinate transformations.
71       */
72      protected VisualizationServer<V,E> vv;
73      
74      /**
75       * The current picking heuristic for this instance.  Defaults
76       * to <code>CENTERED</code>.
77       */
78      protected Style style = Style.CENTERED;
79      
80      /**
81       * Creates a <code>ShapePickSupport</code> for the <code>vv</code>
82       * VisualizationServer, with the specified pick footprint and
83       * the default pick style.
84       * The <code>VisualizationServer</code> is used to access 
85       * properties of the current visualization (layout, renderer,
86       * coordinate transformations, vertex/edge shapes, etc.).
87       * @param vv source of the current <code>Layout</code>.
88       * @param pickSize the size of the pick footprint for line edges
89       */
90      public ShapePickSupport(VisualizationServer<V,E> vv, float pickSize) {
91      	this.vv = vv;
92          this.pickSize = pickSize;
93      }
94      
95      /**
96       * Create a <code>ShapePickSupport</code> for the specified
97       * <code>VisualizationServer</code> with a default pick footprint.
98       * of size 2.
99       * @param vv the visualization server used for rendering
100      */
101     public ShapePickSupport(VisualizationServer<V,E> vv) {
102         this.vv = vv;
103         this.pickSize = 2;
104     }
105     
106     /**
107      * Returns the style of picking used by this instance.
108      * This specifies which of the elements, among those
109      * whose shapes contain the pick point, is returned.
110      * The available styles are:
111      * <ul>
112      * <li><code>Style.CENTERED</code>: returns the element whose 
113      * center is closest to the pick point.
114      * <li><code>Style.LOWEST</code>: returns the first such element
115      * encountered.  (If the element collection has a consistent
116      * ordering, this will also be the element "on the bottom", 
117      * that is, the one which is rendered first.) 
118      * <li><code>Style.HIGHEST</code>: returns the last such element
119      * encountered.  (If the element collection has a consistent
120      * ordering, this will also be the element "on the top", 
121      * that is, the one which is rendered last.)
122      * </ul>
123      * 
124 	 * @return the style of picking used by this instance
125 	 */
126 	public Style getStyle() {
127 		return style;
128 	}
129 
130 	/**
131 	 * Specifies the style of picking to be used by this instance.
132      * This specifies which of the elements, among those
133      * whose shapes contain the pick point, will be returned.
134      * The available styles are:
135      * <ul>
136      * <li><code>Style.CENTERED</code>: returns the element whose 
137      * center is closest to the pick point.
138      * <li><code>Style.LOWEST</code>: returns the first such element
139      * encountered.  (If the element collection has a consistent
140      * ordering, this will also be the element "on the bottom", 
141      * that is, the one which is rendered first.) 
142      * <li><code>Style.HIGHEST</code>: returns the last such element
143      * encountered.  (If the element collection has a consistent
144      * ordering, this will also be the element "on the top", 
145      * that is, the one which is rendered last.)
146      * </ul>
147 	 * @param style the style to set
148 	 */
149 	public void setStyle(Style style) {
150 		this.style = style;
151 	}
152 
153 	/** 
154      * Returns the vertex, if any, whose shape contains (x, y).
155      * If (x,y) is contained in more than one vertex's shape, returns
156      * the vertex whose center is closest to the pick point.
157      * 
158      * @param layout the layout instance that records the positions for all vertices
159      * @param x the x coordinate of the pick point
160      * @param y the y coordinate of the pick point
161      * @return the vertex whose shape contains (x,y), and whose center is closest to the pick point
162      */
163     public V getVertex(Layout<V, E> layout, double x, double y) {
164 
165         V closest = null;
166         double minDistance = Double.MAX_VALUE;
167         Point2D ip = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW, 
168         		new Point2D.Double(x,y));
169         x = ip.getX();
170         y = ip.getY();
171 
172         while(true) {
173             try {
174                 for(V v : getFilteredVertices(layout)) {
175                 	
176                     Shape shape = vv.getRenderContext().getVertexShapeTransformer().apply(v);
177                     // get the vertex location
178                     Point2D p = layout.apply(v);
179                     if(p == null) continue;
180                     // transform the vertex location to screen coords
181                     p = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, p);
182                     
183                     double ox = x - p.getX();
184                     double oy = y - p.getY();
185 
186                     if(shape.contains(ox, oy)) {
187                     	
188                     	if(style == Style.LOWEST) {
189                     		// return the first match
190                     		return v;
191                     	} else if(style == Style.HIGHEST) {
192                     		// will return the last match
193                     		closest = v;
194                     	} else {
195                     		
196                     		// return the vertex closest to the
197                     		// center of a vertex shape
198 	                        Rectangle2D bounds = shape.getBounds2D();
199 	                        double dx = bounds.getCenterX() - ox;
200 	                        double dy = bounds.getCenterY() - oy;
201 	                        double dist = dx * dx + dy * dy;
202 	                        if (dist < minDistance) {
203 	                        	minDistance = dist;
204 	                        	closest = v;
205 	                        }
206                     	}
207                     }
208                 }
209                 break;
210             } catch(ConcurrentModificationException cme) {}
211         }
212         return closest;
213     }
214 
215     /**
216      * Returns the vertices whose layout coordinates are contained in 
217      * <code>Shape</code>.
218      * The shape is in screen coordinates, and the graph vertices
219      * are transformed to screen coordinates before they are tested
220      * for inclusion.
221      * @return the <code>Collection</code> of vertices whose <code>layout</code>
222      * coordinates are contained in <code>shape</code>.
223      */
224     public Collection<V> getVertices(Layout<V, E> layout, Shape shape) {
225     	Set<V> pickedVertices = new HashSet<V>();
226     	
227     	// remove the view transform from the rectangle
228     	shape = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW, shape);
229 
230         while(true) {
231             try {
232                 for(V v : getFilteredVertices(layout)) {
233                     Point2D p = layout.apply(v);
234                     if(p == null) continue;
235 
236                     p = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, p);
237                     if(shape.contains(p)) {
238                     	pickedVertices.add(v);
239                     }
240                 }
241                 break;
242             } catch(ConcurrentModificationException cme) {}
243         }
244         return pickedVertices;
245     }
246     
247     /**
248      * Returns an edge whose shape intersects the 'pickArea' footprint of the passed
249      * x,y, coordinates.
250      * 
251 	 * @param layout the context in which the location is defined
252 	 * @param x the x coordinate of the location
253 	 * @param y the y coordinate of the location
254      * @return an edge whose shape intersects the pick area centered on the location {@code (x,y)}
255      */
256     public E getEdge(Layout<V, E> layout, double x, double y) {
257 
258         Point2D ip = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW, new Point2D.Double(x,y));
259         x = ip.getX();
260         y = ip.getY();
261 
262         // as a Line has no area, we can't always use edgeshape.contains(point) so we
263         // make a small rectangular pickArea around the point and check if the
264         // edgeshape.intersects(pickArea)
265         Rectangle2D pickArea = 
266             new Rectangle2D.Float((float)x-pickSize/2,(float)y-pickSize/2,pickSize,pickSize);
267         E closest = null;
268         double minDistance = Double.MAX_VALUE;
269         while(true) {
270             try {
271                 for(E e : getFilteredEdges(layout)) {
272 
273                     Shape edgeShape = getTransformedEdgeShape(layout, e);
274                     if (edgeShape == null)
275                     	continue;
276 
277                     // because of the transform, the edgeShape is now a GeneralPath
278                     // see if this edge is the closest of any that intersect
279                     if(edgeShape.intersects(pickArea)) {
280                         float cx=0;
281                         float cy=0;
282                         float[] f = new float[6];
283                         PathIterator pi = new GeneralPath(edgeShape).getPathIterator(null);
284                         if(pi.isDone()==false) {
285                             pi.next();
286                             pi.currentSegment(f);
287                             cx = f[0];
288                             cy = f[1];
289                             if(pi.isDone()==false) {
290                                 pi.currentSegment(f);
291                                 cx = f[0];
292                                 cy = f[1];
293                             }
294                         }
295                         float dx = (float) (cx - x);
296                         float dy = (float) (cy - y);
297                         float dist = dx * dx + dy * dy;
298                         if (dist < minDistance) {
299                             minDistance = dist;
300                             closest = e;
301                         }
302                     }
303 		        }
304 		        break;
305 		    } catch(ConcurrentModificationException cme) {}
306 		}
307 		return closest;
308     }
309 
310     /**
311      * Retrieves the shape template for <code>e</code> and
312      * transforms it according to the positions of its endpoints
313      * in <code>layout</code>.
314      * @param layout the <code>Layout</code> which specifies
315      * <code>e</code>'s endpoints' positions
316      * @param e the edge whose shape is to be returned
317      * @return the transformed shape
318      */
319 	private Shape getTransformedEdgeShape(Layout<V, E> layout, E e) {
320 		Pair<V> pair = layout.getGraph().getEndpoints(e);
321 		V v1 = pair.getFirst();
322 		V v2 = pair.getSecond();
323 		boolean isLoop = v1.equals(v2);
324 		Point2D p1 = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, layout.apply(v1));
325 		Point2D p2 = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, layout.apply(v2));
326         if(p1 == null || p2 == null) 
327         	return null;
328 		float x1 = (float) p1.getX();
329 		float y1 = (float) p1.getY();
330 		float x2 = (float) p2.getX();
331 		float y2 = (float) p2.getY();
332 
333 		// translate the edge to the starting vertex
334 		AffineTransform xform = AffineTransform.getTranslateInstance(x1, y1);
335 
336 		Shape edgeShape = vv.getRenderContext().getEdgeShapeTransformer().apply(e);
337 		if(isLoop) {
338 		    // make the loops proportional to the size of the vertex
339 		    Shape s2 = vv.getRenderContext().getVertexShapeTransformer().apply(v2);
340 		    Rectangle2D s2Bounds = s2.getBounds2D();
341 		    xform.scale(s2Bounds.getWidth(),s2Bounds.getHeight());
342 		    // move the loop so that the nadir is centered in the vertex
343 		    xform.translate(0, -edgeShape.getBounds2D().getHeight()/2);
344 		} else {
345 		    float dx = x2 - x1;
346 		    float dy = y2 - y1;
347 		    // rotate the edge to the angle between the vertices
348 		    double theta = Math.atan2(dy,dx);
349 		    xform.rotate(theta);
350 		    // stretch the edge to span the distance between the vertices
351 		    float dist = (float) Math.sqrt(dx*dx + dy*dy);
352 		    xform.scale(dist, 1.0f);
353 		}
354 
355 		// transform the edge to its location and dimensions
356 		edgeShape = xform.createTransformedShape(edgeShape);
357 		return edgeShape;
358 	}
359 
360     protected Collection<V> getFilteredVertices(Layout<V,E> layout) {
361     	if(verticesAreFiltered()) {
362     		Collection<V> unfiltered = layout.getGraph().getVertices();
363     		Collection<V> filtered = new LinkedHashSet<V>();
364     		for(V v : unfiltered) {
365     			if(isVertexRendered(Context.<Graph<V,E>,V>getInstance(layout.getGraph(),v))) {
366     				filtered.add(v);
367     			}
368     		}
369     		return filtered;
370     	} else {
371     		return layout.getGraph().getVertices();
372     	}
373     }
374 
375     protected Collection<E> getFilteredEdges(Layout<V,E> layout) {
376     	if(edgesAreFiltered()) {
377     		Collection<E> unfiltered = layout.getGraph().getEdges();
378     		Collection<E> filtered = new LinkedHashSet<E>();
379     		for(E e : unfiltered) {
380     			if(isEdgeRendered(Context.<Graph<V,E>,E>getInstance(layout.getGraph(),e))) {
381     				filtered.add(e);
382     			}
383     		}
384     		return filtered;
385     	} else {
386     		return layout.getGraph().getEdges();
387     	}
388     }
389     
390     /**
391      * Quick test to allow optimization of <code>getFilteredVertices()</code>.
392      * @return <code>true</code> if there is an active vertex filtering
393      * mechanism for this visualization, <code>false</code> otherwise
394      */
395     protected boolean verticesAreFiltered() {
396 		Predicate<Context<Graph<V,E>,V>> vertexIncludePredicate =
397 			vv.getRenderContext().getVertexIncludePredicate();
398 		return vertexIncludePredicate != null &&
399 			vertexIncludePredicate.equals(Predicates.alwaysTrue()) == false;
400     }
401     
402     /**
403      * Quick test to allow optimization of <code>getFilteredEdges()</code>.
404      * @return <code>true</code> if there is an active edge filtering
405      * mechanism for this visualization, <code>false</code> otherwise
406      */
407     protected boolean edgesAreFiltered() {
408 		Predicate<Context<Graph<V,E>,E>> edgeIncludePredicate =
409 			vv.getRenderContext().getEdgeIncludePredicate();
410 		return edgeIncludePredicate != null &&
411 			edgeIncludePredicate.equals(Predicates.alwaysTrue()) == false;
412     }
413     
414 	/**
415 	 * Returns <code>true</code> if this vertex in this graph is included 
416 	 * in the collections of elements to be rendered, and <code>false</code> otherwise.
417 	 * @param context the vertex and graph to be queried
418 	 * @return <code>true</code> if this vertex is 
419 	 * included in the collections of elements to be rendered, <code>false</code>
420 	 * otherwise.
421 	 */
422 	protected boolean isVertexRendered(Context<Graph<V,E>,V> context) {
423 		Predicate<Context<Graph<V,E>,V>> vertexIncludePredicate =
424 			vv.getRenderContext().getVertexIncludePredicate();
425 		return vertexIncludePredicate == null || vertexIncludePredicate.apply(context);
426 	}
427 	
428 	/**
429 	 * Returns <code>true</code> if this edge and its endpoints
430 	 * in this graph are all included in the collections of
431 	 * elements to be rendered, and <code>false</code> otherwise.
432 	 * @param context the edge and graph to be queried
433 	 * @return <code>true</code> if this edge and its endpoints are all
434 	 * included in the collections of elements to be rendered, <code>false</code>
435 	 * otherwise.
436 	 */
437 	protected boolean isEdgeRendered(Context<Graph<V,E>,E> context) {
438 		Predicate<Context<Graph<V,E>,V>> vertexIncludePredicate =
439 			vv.getRenderContext().getVertexIncludePredicate();
440 		Predicate<Context<Graph<V,E>,E>> edgeIncludePredicate =
441 			vv.getRenderContext().getEdgeIncludePredicate();
442 		Graph<V,E> g = context.graph;
443 		E e = context.element;
444 		boolean edgeTest = edgeIncludePredicate == null || edgeIncludePredicate.apply(context);
445 		Pair<V> endpoints = g.getEndpoints(e);
446 		V v1 = endpoints.getFirst();
447 		V v2 = endpoints.getSecond();
448 		boolean endpointsTest = vertexIncludePredicate == null ||
449 			(vertexIncludePredicate.apply(Context.<Graph<V,E>,V>getInstance(g,v1)) && 
450 					vertexIncludePredicate.apply(Context.<Graph<V,E>,V>getInstance(g,v2)));
451 		return edgeTest && endpointsTest;
452 	}
453 
454 	/**
455 	 * Returns the size of the edge picking area.
456 	 * The picking area is square; the size is specified as the length of one
457 	 * side, in view coordinates. 
458 	 * @return the size of the edge picking area
459 	 */
460 	public float getPickSize() {
461 		return pickSize;
462 	}
463 
464 	/**
465 	 * Sets the size of the edge picking area.
466 	 * @param pickSize the length of one side of the (square) picking area, in view coordinates
467 	 */
468 	public void setPickSize(float pickSize) {
469 		this.pickSize = pickSize;
470 	}
471 
472 }