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 }