/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.gui.swing;
// Geometry
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
// Graphics
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
// Events
import java.awt.event.MouseEvent;
import javax.swing.event.MouseInputAdapter;
/**
* Controller which allows the user to select a region of a component. The user must click on a
* point in the component, then drag the mouse pointer whilst keeping the button pressed. During
* the dragging, the shape which is drawn will normally be a rectangle. Other shapes could always
* be used such as, for example, an ellipse. To use this class, it is necessary to create a derived
* class which defines the following methods:
*
* <ul>
* <li>{@link #selectionPerformed} (obligatory)</li>
* <li>{@link #getModel} (optional)</li>
* </ul>
*
* This controller should then be registered with one, and only one, component
* using the following syntax:
*
* <blockquote><pre>
* {@link Component} component=...
* MouseSelectionTracker control=...
* component.addMouseListener(control);
* </pre></blockquote>
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
abstract class MouseSelectionTracker extends MouseInputAdapter {
/**
* Stippled rectangle representing the region which the user is currently
* selecting. This rectangle can be empty. These coordinates are only
* significant in the period between the user pressing the mouse button
* and then releasing it to outline a region. Conventionally, the
* {@code null} value indicates that a line should be used instead of
* a rectangular shape. The coordinates are always expressed in pixels.
*/
private transient RectangularShape mouseSelectedArea;
/**
* Colour to replace during XOR drawings on a graphic.
* This colour is specified in {@link Graphics2D#setColor}.
*/
private Color backXORColor = Color.white;
/**
* Colour to replace with during the XOR drawings on a graphic.
* This colour is specified in {@link Graphics2D#setXORMode}.
*/
private Color lineXORColor = Color.black;
/**
* <var>x</var> coordinate of the mouse when the button is pressed.
*/
private transient int ox;
/**
* <var>y</var> coordinate of the mouse when the button is pressed.
*/
private transient int oy;
/**
* <var>x</var> coordinate of the mouse during the last drag.
*/
private transient int px;
/**
* <var>y</var> coordinate of the mouse during the last drag.
*/
private transient int py;
/**
* Indicates whether a selection is underway.
*/
private transient boolean isDragging;
/**
* Constructs an object which will allow rectangular regions to be selected using the mouse.
*/
public MouseSelectionTracker() {
}
/**
* Specifies the colours to be used for drawing the outline of a box when
* the user selects a region. All {@code a} colours will be replaced
* by {@code b} colours and vice versa.
*/
public void setXORColors(final Color a, final Color b) {
backXORColor = a;
lineXORColor = b;
}
/**
* Returns the geometric shape to use for marking the boundaries of a region. This shape is
* normally a rectangle but could also be an ellipse, an arrow or even other shapes. The
* coordinates of the returned shape will not be taken into account. In fact, these coordinates
* will regularly be discarded. Only the class of the returned shape will count (for example,
* {@link java.awt.geom.Ellipse2D} vs {@link java.awt.geom.Rectangle2D}) and their parameters
* which are not linked to their position (for example, the rounding of a rectangle's
* corners).
* <p>
* The shape returned will normally be from a class derived from {@link RectangularShape},
* but could also be from the {@link Line2D} class. <strong>Any other class risks throwing a
* {@link ClassCastException} when executed</strong>.
*
* The default implementation always returns an object {@link Rectangle}.
*
* @param event Mouse coordinate when the button is pressed. This information can be used by
* the derived classes which like to be informed of the position of the mouse before
* chosing a geometric shape.
* @return Shape from the class {link RectangularShape} or {link Line2D}, or {@code null}
* to indicate that we do not want to make a selection.
*/
protected Shape getModel(final MouseEvent event) {
return new Rectangle();
}
/**
* Method which is automatically called after the user selects a region with the mouse.
* All coordinates passed in as parameters are expressed in pixels.
*
* @param ox <var>x</var> coordinate of the mouse when the user pressed the mouse button.
* @param oy <var>y</var> coordinate of the mouse when the user pressed the mouse button.
* @param px <var>x</var> coordinate of the mouse when the user released the mouse button.
* @param py <var>y</var> coordinate of the mouse when the user released the mouse button.
*/
protected abstract void selectionPerformed(int ox, int oy, int px, int py);
/**
* Returns the geometric shape surrounding the last region to be selected by the user. An
* optional affine transform can be specified to convert the region selected by the user
* into logical coordinates. The class of the shape returned depends on the model returned by
* {@link #getModel}:
*
* <ul>
* <li>If the model is null (which means that this {@code MouseSelectionTracker} object only
* draws a line between points), the object returned will belong to the {@link Line2D}
* class.</li>
* <li>If the model is not null, the object returned can be from the same class (most often
* {@link java.awt.geom.Rectangle2D}). There could always be situations where the object
* returned is from another class, for example if the affine transform carries out a
* rotation.</li>
* </ul>
*
* @param transform Affine transform which converts logical coordinates into pixel coordinates.
* It is usually an affine transform which is used in a {@code paint(...)} method to
* draw shapes expressed in logical coordinates.
* @return A geometric shape enclosing the last region to be selected by the user, or
* {@code null} if no selection has yet been made.
* @throws NoninvertibleTransformException If the affine transform can't be inverted.
*/
public Shape getSelectedArea(final AffineTransform transform) throws NoninvertibleTransformException {
if (ox == px && oy == py) {
return null;
}
RectangularShape shape = mouseSelectedArea;
if (transform != null && !transform.isIdentity()) {
if (shape == null) {
final Point2D.Float po = new Point2D.Float(ox, oy);
final Point2D.Float pp = new Point2D.Float(px, py);
transform.inverseTransform(po, po);
transform.inverseTransform(pp, pp);
return new Line2D.Float(po, pp);
} else {
if (canReshape(shape, transform)) {
final Point2D.Double point = new Point2D.Double();
double xmin = Double.POSITIVE_INFINITY;
double ymin = Double.POSITIVE_INFINITY;
double xmax = Double.NEGATIVE_INFINITY;
double ymax = Double.NEGATIVE_INFINITY;
for (int i = 0; i < 4; i++) {
point.x = (i&1) == 0 ? shape.getMinX() : shape.getMaxX();
point.y = (i&2) == 0 ? shape.getMinY() : shape.getMaxY();
transform.inverseTransform(point, point);
if (point.x < xmin) xmin = point.x;
if (point.x > xmax) xmax = point.x;
if (point.y < ymin) ymin = point.y;
if (point.y > ymax) ymax = point.y;
}
if (shape instanceof Rectangle) {
return new Rectangle2D.Float((float) xmin,
(float) ymin,
(float) (xmax - xmin),
(float) (ymax - ymin));
} else {
shape = (RectangularShape) shape.clone();
shape.setFrame(xmin, ymin, xmax - xmin, ymax - ymin);
return shape;
}
}
else {
return transform.createInverse().createTransformedShape(shape);
}
}
}
else {
return (shape != null) ? (Shape) shape.clone() : new Line2D.Float(ox, oy, px, py);
}
}
/**
* Indicates whether we can transform {@code shape} simply by calling its
* {@code shape.setFrame(...)} method rather than by using the heavy artillery
* that is the {@code transform.createTransformedShape(shape)} method.
*/
private static boolean canReshape(final RectangularShape shape,
final AffineTransform transform) {
final int type=transform.getType();
if ((type & AffineTransform.TYPE_GENERAL_TRANSFORM) != 0) return false;
if ((type & AffineTransform.TYPE_MASK_ROTATION) != 0) return false;
if ((type & AffineTransform.TYPE_FLIP) != 0) {
if (shape instanceof Rectangle2D) return true;
if (shape instanceof Ellipse2D) return true;
if (shape instanceof RoundRectangle2D) return true;
return false;
}
return true;
}
/**
* Returns a {@link Graphics2D} object to be used for drawing in the specified component. We
* must not forget to call {@link Graphics2D#dispose} when the graphics object is no longer
* needed.
*/
private Graphics2D getGraphics(final Component c) {
final Graphics2D graphics = (Graphics2D) c.getGraphics();
graphics.setXORMode(lineXORColor);
graphics.setColor (backXORColor);
return graphics;
}
/**
* Informs this controller that the mouse button has been pressed.
* The default implementation retains the mouse coordinate (which will
* become one of the corners of the future rectangle to be drawn)
* and prepares {@code this} to observe the mouse movements.
*
* @throws ClassCastException if {@link #getModel} doesn't return a shape
* from the class {link RectangularShape} or {link Line2D}.
*/
public void mousePressed(final MouseEvent event) throws ClassCastException {
if (!event.isConsumed() && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
final Component source = event.getComponent();
if (source != null) {
Shape model = getModel(event);
if (model != null) {
isDragging = true;
ox = px = event.getX();
oy = py = event.getY();
if (model instanceof Line2D) {
model = null;
}
mouseSelectedArea = (RectangularShape) model;
if (mouseSelectedArea != null) {
mouseSelectedArea.setFrame(ox, oy, 0, 0);
}
source.addMouseMotionListener(this);
}
source.requestFocus();
event.consume();
}
}
}
/**
* Informs this controller that the mouse has been dragged. The default
* implementation uses this to move a corner of the rectangle used to
* select the region. The other corner remains fixed at the point
* where the mouse was at the moment its button was pressed..
*/
public void mouseDragged(final MouseEvent event) {
if (isDragging) {
final Graphics2D graphics = getGraphics(event.getComponent());
if (mouseSelectedArea == null) {
graphics.drawLine(ox, oy, px, py);
px = event.getX();
py = event.getY();
graphics.drawLine(ox, oy, px, py);
} else {
graphics.draw(mouseSelectedArea);
int xmin = this.ox;
int ymin = this.oy;
int xmax = px = event.getX();
int ymax = py = event.getY();
if (xmin > xmax) {
final int xtmp = xmin;
xmin = xmax; xmax = xtmp;
}
if (ymin > ymax) {
final int ytmp = ymin;
ymin = ymax; ymax = ytmp;
}
mouseSelectedArea.setFrame(xmin, ymin, xmax - xmin, ymax - ymin);
graphics.draw(mouseSelectedArea);
}
graphics.dispose();
event.consume();
}
}
/**
* Informs this controller that the mouse button has been released.
* The default implementation calls {@link #selectionPerformed} with
* the bounds of the selected region as parameters.
*/
public void mouseReleased(final MouseEvent event) {
if (isDragging && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
isDragging = false;
final Component component = event.getComponent();
component.removeMouseMotionListener(this);
final Graphics2D graphics = getGraphics(event.getComponent());
if (mouseSelectedArea == null) {
graphics.drawLine(ox, oy, px, py);
} else {
graphics.draw(mouseSelectedArea);
}
graphics.dispose();
px = event.getX();
py = event.getY();
selectionPerformed(ox, oy, px, py);
event.consume();
}
}
/**
* Informs this controller that the mouse has been moved but not as a
* result of the user selecting a region. The default implementation
* signals to the source component that {@code this} is no longer
* interested in being informed about mouse movements.
*/
public void mouseMoved(final MouseEvent event) {
// Normally not necessary, but it seems that this "listener"
// sometimes stays in place when it shouldn't.
event.getComponent().removeMouseMotionListener(this);
}
}