edu.uci.ics.jung.visualization.picking.ShapePickSupport.java Source code

Java tutorial

Introduction

Here is the source code for edu.uci.ics.jung.visualization.picking.ShapePickSupport.java

Source

/*
 * Copyright (c) 2005, the JUNG Project and the Regents of the University 
 * of California
 * All rights reserved.
 *
 * This software is open-source under the BSD license; see either
 * "license.txt" or
 * http://jung.sourceforge.net/license.txt for a description.
 * Created on Mar 11, 2005
 *
 */
package edu.uci.ics.jung.visualization.picking;

import java.awt.Shape;
import java.awt.geom.AffineTransform;
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.LinkedHashSet;
import java.util.Set;

import org.apache.commons.collections15.Predicate;
import org.apache.commons.collections15.functors.TruePredicate;

import edu.uci.ics.jung.algorithms.layout.GraphElementAccessor;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.graph.util.Pair;
import edu.uci.ics.jung.visualization.Layer;
import edu.uci.ics.jung.visualization.VisualizationServer;

/**
 * A <code>GraphElementAccessor</code> that returns elements whose <code>Shape</code>
 * contains the specified pick point or region.
 * 
 * @author Tom Nelson
 *
 */
public class ShapePickSupport<V, E> implements GraphElementAccessor<V, E> {

    /**
     * The available picking heuristics:
      * <ul>
      * <li/><code>Style.CENTERED</code>: returns the element whose 
      * center is closest to the pick point.
      * <li/><code>Style.LOWEST</code>: 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.) 
      * <li/><code>Style.HIGHEST</code>: 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.)
      * </ul>
     *
     */
    public static enum Style {
        LOWEST, CENTERED, HIGHEST
    };

    /**
     * 
     *
     */
    protected float pickSize;

    /**
     * The <code>VisualizationServer</code> in which the 
     * this instance is being used for picking.  Used to 
     * retrieve properties such as the layout, renderer,
     * vertex and edge shapes, and coordinate transformations.
     */
    protected VisualizationServer<V, E> vv;

    /**
     * The current picking heuristic for this instance.  Defaults
     * to <code>CENTERED</code>.
     */
    protected Style style = Style.CENTERED;

    /**
     * Creates a <code>ShapePickSupport</code> for the <code>vv</code>
     * VisualizationServer, with the specified pick footprint and
     * the default pick style.
     * The <code>VisualizationServer</code> is used to access 
     * properties of the current visualization (layout, renderer,
     * coordinate transformations, vertex/edge shapes, etc.).
     * @param vv source of the current <code>Layout</code>.
     * @param pickSize the size of the pick footprint for line edges
     */
    public ShapePickSupport(VisualizationServer<V, E> vv, float pickSize) {
        this.vv = vv;
        this.pickSize = pickSize;
    }

    /**
     * Create a <code>ShapePickSupport</code> for the specified
     * <code>VisualizationServer</code> with a default pick footprint.
     * of size 2.
     */
    public ShapePickSupport(VisualizationServer<V, E> 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:
     * <ul>
     * <li/><code>Style.CENTERED</code>: returns the element whose 
     * center is closest to the pick point.
     * <li/><code>Style.LOWEST</code>: 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.) 
     * <li/><code>Style.HIGHEST</code>: 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.)
     * </ul>
     * 
    * @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:
      * <ul>
      * <li/><code>Style.CENTERED</code>: returns the element whose 
      * center is closest to the pick point.
      * <li/><code>Style.LOWEST</code>: 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.) 
      * <li/><code>Style.HIGHEST</code>: 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.)
      * </ul>
     * @param style the style to set
     */
    public void setStyle(Style style) {
        this.style = style;
    }

    /** 
      * Iterates over Vertices, checking to see if x,y is contained in the
      * Vertex's Shape. If (x,y) is contained in more than one vertex, use
      * the vertex whose center is closest to the pick point.
      * @see edu.uci.ics.jung.visualization.picking.PickSupport#getVertex(double, double)
      */
    public V getVertex(Layout<V, E> layout, double x, double y) {

        V closest = null;
        double minDistance = Double.MAX_VALUE;
        Point2D ip = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW,
                new Point2D.Double(x, y));
        x = ip.getX();
        y = ip.getY();

        while (true) {
            try {
                for (V v : getFilteredVertices(layout)) {

                    Shape shape = vv.getRenderContext().getVertexShapeTransformer().transform(v);
                    // get the vertex location
                    Point2D p = layout.transform(v);
                    if (p == null)
                        continue;
                    // transform the vertex location to screen coords
                    p = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, p);

                    double ox = x - p.getX();
                    double oy = y - p.getY();

                    if (shape.contains(ox, oy)) {

                        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 vertex closest to the
                            // center of a vertex shape
                            Rectangle2D bounds = shape.getBounds2D();
                            double dx = bounds.getCenterX() - ox;
                            double dy = bounds.getCenterY() - oy;
                            double dist = dx * dx + dy * dy;
                            if (dist < minDistance) {
                                minDistance = dist;
                                closest = v;
                            }
                        }
                    }
                }
                break;
            } catch (ConcurrentModificationException cme) {
            }
        }
        return closest;
    }

    /**
     * Returns the vertices whose layout coordinates are contained in 
     * <code>Shape</code>.
     * The shape is in screen coordinates, and the graph vertices
     * are transformed to screen coordinates before they are tested
     * for inclusion.
     * @return the <code>Collection</code> of vertices whose <code>layout</code>
     * coordinates are contained in <code>shape</code>.
     */
    public Collection<V> getVertices(Layout<V, E> layout, Shape shape) {
        Set<V> pickedVertices = new HashSet<V>();

        // remove the view transform from the rectangle
        shape = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW, shape);

        while (true) {
            try {
                for (V v : getFilteredVertices(layout)) {
                    Point2D p = layout.transform(v);
                    if (p == null)
                        continue;

                    p = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, p);
                    if (shape.contains(p)) {
                        pickedVertices.add(v);
                    }
                }
                break;
            } catch (ConcurrentModificationException cme) {
            }
        }
        return pickedVertices;
    }

    /**
     * Returns an edge whose shape intersects the 'pickArea' footprint of the passed
     * x,y, coordinates.
     */
    public E getEdge(Layout<V, E> layout, double x, double y) {

        Point2D ip = vv.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW,
                new Point2D.Double(x, y));
        x = ip.getX();
        y = ip.getY();

        // 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;
        while (true) {
            try {
                for (E e : getFilteredEdges(layout)) {

                    Shape edgeShape = getTransformedEdgeShape(layout, 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;
    }

    /**
     * Retrieves the shape template for <code>e</code> and
     * transforms it according to the positions of its endpoints
     * in <code>layout</code>.
     * @param layout the <code>Layout</code> which specifies
     * <code>e</code>'s endpoints' positions
     * @param e the edge whose shape is to be returned
     * @return
     */
    private Shape getTransformedEdgeShape(Layout<V, E> layout, E e) {
        Pair<V> pair = layout.getGraph().getEndpoints(e);
        V v1 = pair.getFirst();
        V v2 = pair.getSecond();
        boolean isLoop = v1.equals(v2);
        Point2D p1 = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v1));
        Point2D p2 = vv.getRenderContext().getMultiLayerTransformer().transform(Layer.LAYOUT, layout.transform(v2));
        if (p1 == null || p2 == null)
            return null;
        float x1 = (float) p1.getX();
        float y1 = (float) p1.getY();
        float x2 = (float) p2.getX();
        float y2 = (float) p2.getY();

        // translate the edge to the starting vertex
        AffineTransform xform = AffineTransform.getTranslateInstance(x1, y1);

        Shape edgeShape = vv.getRenderContext().getEdgeShapeTransformer()
                .transform(Context.<Graph<V, E>, E>getInstance(vv.getGraphLayout().getGraph(), e));
        if (isLoop) {
            // make the loops proportional to the size of the vertex
            Shape s2 = vv.getRenderContext().getVertexShapeTransformer().transform(v2);
            Rectangle2D s2Bounds = s2.getBounds2D();
            xform.scale(s2Bounds.getWidth(), s2Bounds.getHeight());
            // move the loop so that the nadir is centered in the vertex
            xform.translate(0, -edgeShape.getBounds2D().getHeight() / 2);
        } else {
            float dx = x2 - x1;
            float dy = y2 - y1;
            // rotate the edge to the angle between the vertices
            double theta = Math.atan2(dy, dx);
            xform.rotate(theta);
            // stretch the edge to span the distance between the vertices
            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;
    }

    /**
     * 
     * @param layout
     * @return
     */
    protected Collection<V> getFilteredVertices(Layout<V, E> layout) {
        if (verticesAreFiltered()) {
            Collection<V> unfiltered = layout.getGraph().getVertices();
            Collection<V> filtered = new LinkedHashSet<V>();
            for (V v : unfiltered) {
                if (isVertexRendered(Context.<Graph<V, E>, V>getInstance(layout.getGraph(), v))) {
                    filtered.add(v);
                }
            }
            return filtered;
        } else {
            return layout.getGraph().getVertices();
        }
    }

    /**
     * 
     * @param layout
     * @return
     */
    protected Collection<E> getFilteredEdges(Layout<V, E> layout) {
        if (edgesAreFiltered()) {
            Collection<E> unfiltered = layout.getGraph().getEdges();
            Collection<E> filtered = new LinkedHashSet<E>();
            for (E e : unfiltered) {
                if (isEdgeRendered(Context.<Graph<V, E>, E>getInstance(layout.getGraph(), e))) {
                    filtered.add(e);
                }
            }
            return filtered;
        } else {
            return layout.getGraph().getEdges();
        }
    }

    /**
     * Quick test to allow optimization of <code>getFilteredVertices()</code>.
     * @return <code>true</code> if there is an active vertex filtering
     * mechanism for this visualization, <code>false</code> otherwise
     */
    protected boolean verticesAreFiltered() {
        Predicate<Context<Graph<V, E>, V>> vertexIncludePredicate = vv.getRenderContext()
                .getVertexIncludePredicate();
        return vertexIncludePredicate != null && vertexIncludePredicate instanceof TruePredicate == false;
    }

    /**
     * Quick test to allow optimization of <code>getFilteredEdges()</code>.
     * @return <code>true</code> if there is an active edge filtering
     * mechanism for this visualization, <code>false</code> otherwise
     */
    protected boolean edgesAreFiltered() {
        Predicate<Context<Graph<V, E>, E>> edgeIncludePredicate = vv.getRenderContext().getEdgeIncludePredicate();
        return edgeIncludePredicate != null && edgeIncludePredicate instanceof TruePredicate == false;
    }

    /**
     * Returns <code>true</code> if this vertex in this graph is included 
     * in the collections of elements to be rendered, and <code>false</code> otherwise.
     * @param context the vertex and graph to be queried
     * @return <code>true</code> if this vertex is 
     * included in the collections of elements to be rendered, <code>false</code>
     * otherwise.
     */
    protected boolean isVertexRendered(Context<Graph<V, E>, V> context) {
        Predicate<Context<Graph<V, E>, V>> vertexIncludePredicate = vv.getRenderContext()
                .getVertexIncludePredicate();
        return vertexIncludePredicate == null || vertexIncludePredicate.evaluate(context);
    }

    /**
     * Returns <code>true</code> if this edge and its endpoints
     * in this graph are all included in the collections of
     * elements to be rendered, and <code>false</code> otherwise.
     * @param context the edge and graph to be queried
     * @return <code>true</code> if this edge and its endpoints are all
     * included in the collections of elements to be rendered, <code>false</code>
     * otherwise.
     */
    protected boolean isEdgeRendered(Context<Graph<V, E>, E> context) {
        Predicate<Context<Graph<V, E>, V>> vertexIncludePredicate = vv.getRenderContext()
                .getVertexIncludePredicate();
        Predicate<Context<Graph<V, E>, E>> edgeIncludePredicate = vv.getRenderContext().getEdgeIncludePredicate();
        Graph<V, E> g = context.graph;
        E e = context.element;
        boolean edgeTest = edgeIncludePredicate == null || edgeIncludePredicate.evaluate(context);
        Pair<V> endpoints = g.getEndpoints(e);
        V v1 = endpoints.getFirst();
        V v2 = endpoints.getSecond();
        boolean endpointsTest = vertexIncludePredicate == null
                || (vertexIncludePredicate.evaluate(Context.<Graph<V, E>, V>getInstance(g, v1))
                        && vertexIncludePredicate.evaluate(Context.<Graph<V, E>, V>getInstance(g, v2)));
        return edgeTest && endpointsTest;
    }

    /**
     * Returns the size of the edge picking area.
     * The picking area is square; the size is specified as the length of one
     * side, in view coordinates. 
     * @return the size of the edge picking area
     */
    public float getPickSize() {
        return pickSize;
    }

    /**
     * Sets the size of the edge picking area.
     * @param the length of one side of the (square) picking area, in view coordinates
     */
    public void setPickSize(float pickSize) {
        this.pickSize = pickSize;
    }

}