0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
0005: * (C) 2001, Institut de Recherche pour le Développement
0006: *
0007: * This library is free software; you can redistribute it and/or
0008: * modify it under the terms of the GNU Lesser General Public
0009: * License as published by the Free Software Foundation; either
0010: * version 2.1 of the License, or (at your option) any later version.
0011: *
0012: * This library is distributed in the hope that it will be useful,
0013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0015: * Lesser General Public License for more details.
0016: */
0017: package org.geotools.gui.swing;
0018:
0019: // Events and action
0020: import java.util.EventListener;
0021: import java.awt.event.KeyEvent;
0022: import java.awt.event.MouseEvent;
0023: import java.awt.event.ActionEvent;
0024: import java.awt.event.ActionListener;
0025: import java.awt.event.ComponentEvent;
0026: import java.awt.event.ComponentListener;
0027: import java.awt.event.MouseWheelListener;
0028: import java.awt.event.MouseWheelEvent;
0029: import java.awt.event.MouseListener;
0030: import java.awt.event.MouseAdapter;
0031: import javax.swing.AbstractAction;
0032: import javax.swing.Action;
0033: import javax.swing.InputMap;
0034: import javax.swing.ActionMap;
0035: import javax.swing.KeyStroke;
0036: import javax.swing.event.ChangeEvent;
0037: import javax.swing.event.ChangeListener;
0038: import java.beans.PropertyChangeEvent;
0039: import java.beans.PropertyChangeListener;
0040: import org.geotools.gui.swing.event.ZoomChangeEvent;
0041: import org.geotools.gui.swing.event.ZoomChangeListener;
0042:
0043: // Geometry
0044: import java.awt.Shape;
0045: import java.awt.Point;
0046: import java.awt.Insets;
0047: import java.awt.Polygon;
0048: import java.awt.Dimension;
0049: import java.awt.Rectangle;
0050: import java.awt.geom.Line2D;
0051: import java.awt.geom.Point2D;
0052: import java.awt.geom.Ellipse2D;
0053: import java.awt.geom.Dimension2D;
0054: import java.awt.geom.Rectangle2D;
0055: import java.awt.geom.RoundRectangle2D;
0056: import java.awt.geom.RectangularShape;
0057: import java.awt.geom.AffineTransform;
0058: import java.awt.geom.NoninvertibleTransformException;
0059: import org.geotools.referencing.operation.matrix.XAffineTransform;
0060: import org.geotools.resources.geometry.XDimension2D;
0061:
0062: // Graphics
0063: import java.awt.Paint;
0064: import java.awt.Color;
0065: import java.awt.Stroke;
0066: import java.awt.Window;
0067: import java.awt.Graphics;
0068: import java.awt.Graphics2D;
0069: import java.awt.BasicStroke;
0070:
0071: // User interface (AWT)
0072: import java.awt.Toolkit;
0073: import java.awt.Component;
0074: import java.awt.Container;
0075: import java.awt.GridBagLayout;
0076: import java.awt.GridBagConstraints;
0077:
0078: // User interface (Swing)
0079: import javax.swing.JFrame;
0080: import javax.swing.JPanel;
0081: import javax.swing.JMenu;
0082: import javax.swing.JMenuItem;
0083: import javax.swing.JPopupMenu;
0084: import javax.swing.JComponent;
0085: import javax.swing.JViewport;
0086: import javax.swing.JScrollBar;
0087: import javax.swing.JScrollPane;
0088: import javax.swing.AbstractButton;
0089: import javax.swing.SwingConstants;
0090: import javax.swing.SwingUtilities;
0091: import javax.swing.ScrollPaneLayout;
0092: import javax.swing.BoundedRangeModel;
0093: import javax.swing.plaf.ComponentUI;
0094:
0095: // Logging
0096: import java.util.logging.Level;
0097: import java.util.logging.Logger;
0098: import java.util.logging.LogRecord;
0099: import org.geotools.util.logging.Logging;
0100:
0101: // Miscellaneous
0102: import java.util.Arrays;
0103: import java.io.Serializable;
0104: import org.geotools.resources.i18n.Errors;
0105: import org.geotools.resources.i18n.ErrorKeys;
0106: import org.geotools.resources.i18n.Vocabulary;
0107: import org.geotools.resources.i18n.VocabularyKeys;
0108:
0109: /**
0110: * Base class for widget with a zoomable content. User can perform zooms using keyboard, menu
0111: * or mouse. {@code ZoomPane} is an abstract class. Subclass must override at least two methods:
0112: * <p>
0113: * <ul>
0114: * <li>{@link #getArea()}, which must return a bounding box for the content to paint. This
0115: * area can be expressed in arbitrary units. For example, an object wanting to display a
0116: * geographic map with a content ranging from 10° to 15°E and 40° to 45°N should override
0117: * this method as follows:
0118: *
0119: * <pre>
0120: * public Rectangle2D getArea() {
0121: * return new Rectangle2D.Double(10, 40, 5, 5);
0122: * }
0123: * </pre></li>
0124: *
0125: * <li>{@link #paintComponent(Graphics2D)}, which must paint the widget
0126: * content. Implementation must invoke
0127: *
0128: * <code>graphics.transform({link #zoom})</code>
0129: *
0130: * somewhere in its code in order to perform the zoom. Note that, by default, the
0131: * {@linkplain #zoom} is initialized in such a way that the <var>y</var> axis points upwards,
0132: * like the convention in geometry. This is as opposed to the default Java2D axis orientation,
0133: * where the <var>y</var> axis points downwards. If the implementation wants to paint text,
0134: * it should do this with the default transform. Example:
0135: *
0136: * <pre>
0137: * protected void paintComponent(final Graphics2D graphics) {
0138: * graphics.clip({link #getZoomableBounds getZoomableBounds}(null));
0139: * final AffineTransform textTr = graphics.getTransform();
0140: * graphics.transform({link #zoom});
0141: * <strong>
0142: * // Paint the widget here, using logical coordinates. The
0143: * // coordinate system is the same as {@link #getArea()}'s one.
0144: * </strong>
0145: * graphics.setTransform(textTr);
0146: * <strong>
0147: * // Paint any text here, in <em>pixel</em> coordinates.
0148: * </strong>
0149: * }
0150: * </pre></li>
0151: * </ul>
0152: * <p>
0153: * Subclass can also override {@link #reset}, which sets up the initial {@linkplain #zoom}. The
0154: * default implementation sets up the initial zoom in such a way that the following relations are
0155: * approximately held:
0156: *
0157: * <blockquote><cite>
0158: * Logical coordinates provided by {@link #getPreferredArea()}, after an affine transform described
0159: * by {@link #zoom}, match pixel coordinates provided by {@link #getZoomableBounds(Rectangle)}.
0160: * </cite></blockquote>
0161: *
0162: * <p>
0163: * The "preferred area" is initially the same as {@link #getArea()}. The user can specify a
0164: * different preferred area with {@link #setPreferredArea}. The user can also reduce zoomable
0165: * bounds by inserting an empty border around the widget, e.g.:
0166: *
0167: * <pre>
0168: * setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));
0169: * </pre>
0170: *
0171: * <p> </p>
0172: * <h2>Zoom actions</h2>
0173: * Whatever action is performed by the user, all zoom commands are translated as calls to
0174: * {@link #transform}. Derived classes can redefine this method if they want to take particular
0175: * actions during zooms, for example, modifying the minimum and maximum of a graph's axes.
0176: * The table below shows the keyboard presses assigned to each zoom:
0177: *
0178: * <P><TABLE ALIGN="CENTER" BORDER="2">
0179: * <TR BGCOLOR="#CCCCFF"><TH>Key</TH> <TH>Purpose</TH> <TH>{@link Action} name</TH></TR>
0180: * <TR><TD><IMG SRC="doc-files/key-up.png"></TD> <TD>Scroll up</TD> <TD><code>"Up"</code></TD></TR>
0181: * <TR><TD><IMG SRC="doc-files/key-down.png"></TD> <TD>Scroll down</TD> <TD><code>"Down"</code></TD></TR>
0182: * <TR><TD><IMG SRC="doc-files/key-left.png"></TD> <TD>Scroll left</TD> <TD><code>"Left"</code></TD></TR>
0183: * <TR><TD><IMG SRC="doc-files/key-right.png"></TD> <TD>Scroll right</TD><TD><code>"Right"</code></TD></TR>
0184: * <TR><TD><IMG SRC="doc-files/key-pageDown.png"></TD><TD>Zoom in</TD> <TD><code>"ZoomIn"</code></TD></TR>
0185: * <TR><TD><IMG SRC="doc-files/key-pageUp.png"></TD> <TD>Zoom out</TD> <TD><code>"ZoomOut"</code></TD></TR>
0186: * <TR><TD><IMG SRC="doc-files/key-end.png"></TD> <TD>Zoom</TD> <TD><code>"Zoom"</code></TD></TR>
0187: * <TR><TD><IMG SRC="doc-files/key-home.png"></TD> <TD>Default zoom</TD><TD><code>"Reset"</code></TD></TR>
0188: *
0189: * <TR><TD>Ctrl+<IMG SRC="doc-files/key-left.png"></TD> <TD>Anti-clockwise rotation</TD><TD><code>"RotateLeft"</code></TD></TR>
0190: * <TR><TD>Ctrl+<IMG SRC="doc-files/key-right.png"></TD><TD>Clockwise rotation</TD> <TD><code>"RotateRight"</code></TD></TR>
0191: * </TABLE></P>
0192: *
0193: * In this table, the last column gives the Strings by which the different actions
0194: * which manage the zooms. For example, to zoom in, we must write
0195: * <code>{@link #getActionMap() getActionMap()}.get("ZoomIn")</code>.
0196: *
0197: * <p><strong>Note: {@link JScrollPane} objects are not suitable for adding scrollbars to a
0198: * {@code ZoomPane}object.</strong> Instead, use {@link #createScrollPane}. Once again, all
0199: * movements performed by the user through the scrollbars will be translated by calls to
0200: * {@link #transform}.</p>
0201: *
0202: * @since 2.0
0203: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/ZoomPane.java $
0204: * @version $Id: ZoomPane.java 27862 2007-11-12 19:51:19Z desruisseaux $
0205: * @author Martin Desruisseaux
0206: */
0207: public abstract class ZoomPane extends JComponent implements
0208: DeformableViewer {
0209: /**
0210: * Minimum width and height of this component.
0211: */
0212: private static final int MINIMUM_SIZE = 10;
0213:
0214: /**
0215: * Default width and height of this component.
0216: */
0217: private static final int DEFAULT_SIZE = 400;
0218:
0219: /**
0220: * Default width and height of the magnifying glass.
0221: */
0222: private static final int DEFAULT_MAGNIFIER_SIZE = 150;
0223:
0224: /**
0225: * Default color with which to tint magnifying glass.
0226: */
0227: private static final Paint DEFAULT_MAGNIFIER_GLASS = new Color(197,
0228: 204, 221);
0229:
0230: /**
0231: * Default color of the magnifying glass's border.
0232: */
0233: private static final Paint DEFAULT_MAGNIFIER_BORDER = new Color(
0234: 102, 102, 153);
0235:
0236: /**
0237: * Constant indicating the scale changes on the <var>x</var> axis.
0238: */
0239: public static final int SCALE_X = (1 << 0);
0240:
0241: /**
0242: * Constant indicating the scale changes on the <var>y</var> axis.
0243: */
0244: public static final int SCALE_Y = (1 << 1);
0245:
0246: /**
0247: * Constant indicating the scale changes on the <var>x</var> and <var>y</var> axes, with the
0248: * added condition that these changes must be uniform. This flag combines {@link #SCALE_X}
0249: * and {@link #SCALE_Y}. The inverse, however, (<code>{@link #SCALE_X}|{@link #SCALE_Y}</code>)
0250: * doesn't imply {@code UNIFORM_SCALE}.
0251: */
0252: public static final int UNIFORM_SCALE = SCALE_X | SCALE_Y
0253: | (1 << 2);
0254:
0255: /**
0256: * Constant indicating the translations on the <var>x</var> axis.
0257: */
0258: public static final int TRANSLATE_X = (1 << 3);
0259:
0260: /**
0261: * Constant indicating the translations on the <var>y</var> axis.
0262: */
0263: public static final int TRANSLATE_Y = (1 << 4);
0264:
0265: /**
0266: * Constant indicating a rotation.
0267: */
0268: public static final int ROTATE = (1 << 5);
0269:
0270: /**
0271: * Constant indicating the resetting of scale, rotation and translation to a default value
0272: * which makes the whole graphic appear in a window. This command is translated by a call
0273: * to {@link #reset}.
0274: */
0275: public static final int RESET = (1 << 6);
0276:
0277: /**
0278: * Constant indicating default zoom close to the maximum permitted zoom. This zoom should
0279: * allow details of the graphic to be seen without being overly big.
0280: * Note: this flag will only have any effect if at least one of the
0281: * {@link #SCALE_X} and {@link #SCALE_Y} flags is not also specified.
0282: */
0283: public static final int DEFAULT_ZOOM = (1 << 7);
0284:
0285: /**
0286: * Mask representing the combination of all flags.
0287: */
0288: private static final int MASK = SCALE_X | SCALE_Y | UNIFORM_SCALE
0289: | TRANSLATE_X | TRANSLATE_Y | ROTATE | RESET | DEFAULT_ZOOM;
0290:
0291: /**
0292: * Number of pixels by which to move the content of {@code ZoomPane} during translations.
0293: */
0294: private static final double AMOUNT_TRANSLATE = 10;
0295:
0296: /**
0297: * Zoom factor. This factor must be greater than 1.
0298: */
0299: private static final double AMOUNT_SCALE = 1.03125;
0300:
0301: /**
0302: * Rotation angle.
0303: */
0304: private static final double AMOUNT_ROTATE = Math.PI / 90;
0305:
0306: /**
0307: * Factor by which to multiply the {@link #ACTION_AMOUNT} numbers
0308: * when the "Shift" key is kept pressed.
0309: */
0310: private static final double ENHANCEMENT_FACTOR = 7.5;
0311:
0312: /**
0313: * Flag indicating that a paint is in progress.
0314: */
0315: private static final int IS_PAINTING = 0;
0316:
0317: /**
0318: * Flag indicating that a paint of the magnifying glass is in progress.
0319: */
0320: private static final int IS_PAINTING_MAGNIFIER = 1;
0321:
0322: /**
0323: * Flat indicating that a print is in progress.
0324: */
0325: private static final int IS_PRINTING = 2;
0326:
0327: /**
0328: * List of keys which will identify the zoom actions. These keys also identify the resources
0329: * to use in order to make the description appear in the user's language.
0330: */
0331: private static final String[] ACTION_ID = {
0332: /*[0] Left */"Left",
0333: /*[1] Right */"Right",
0334: /*[2] Up */"Up",
0335: /*[3] Down */"Down",
0336: /*[4] ZoomIn */"ZoomIn",
0337: /*[5] ZoomOut */"ZoomOut",
0338: /*[6] ZoomMax */"ZoomMax",
0339: /*[7] Reset */"Reset",
0340: /*[8] RotateLeft */"RotateLeft",
0341: /*[9] RotateRight */"RotateRight" };
0342:
0343: /**
0344: * List of resource keys, to construct the menus in the user's language.
0345: */
0346: private static final int[] RESOURCE_ID = {
0347: /*[0] Left */VocabularyKeys.LEFT,
0348: /*[1] Right */VocabularyKeys.RIGHT,
0349: /*[2] Up */VocabularyKeys.UP,
0350: /*[3] Down */VocabularyKeys.DOWN,
0351: /*[4] ZoomIn */VocabularyKeys.ZOOM_IN,
0352: /*[5] ZoomOut */VocabularyKeys.ZOOM_OUT,
0353: /*[6] ZoomMax */VocabularyKeys.ZOOM_MAX,
0354: /*[7] Reset */VocabularyKeys.RESET,
0355: /*[8] RotateLeft */VocabularyKeys.ROTATE_LEFT,
0356: /*[9] RotateRight */VocabularyKeys.ROTATE_RIGHT };
0357:
0358: /**
0359: * The logger for zoom events.
0360: */
0361: private static final Logger LOGGER = Logging
0362: .getLogger("org.geotools.gui.swing");
0363:
0364: /**
0365: * List of default keystrokes used to perform zooms. The elements of this table go in pairs.
0366: * The even indexes indicate the keystroke whilst the odd indexes indicate the modifier
0367: * (CTRL or SHIFT for example). To obtain the {@link KeyStroke} object for a numbered action
0368: * <var>i</var>, we can use the following code:
0369: *
0370: * <blockquote><pre>
0371: * final int key=DEFAULT_KEYBOARD[(i << 1)+0];
0372: * final int mdf=DEFAULT_KEYBOARD[(i << 1)+1];
0373: * KeyStroke stroke=KeyStroke.getKeyStroke(key, mdf);
0374: * </pre></blockquote>
0375: */
0376: private static final int[] ACTION_KEY = {
0377: /*[0] Left */KeyEvent.VK_LEFT, 0,
0378: /*[1] Right */KeyEvent.VK_RIGHT, 0,
0379: /*[2] Up */KeyEvent.VK_UP, 0,
0380: /*[3] Down */KeyEvent.VK_DOWN, 0,
0381: /*[4] ZoomIn */KeyEvent.VK_PAGE_UP, 0,
0382: /*[5] ZoomOut */KeyEvent.VK_PAGE_DOWN, 0,
0383: /*[6] ZoomMax */KeyEvent.VK_END, 0,
0384: /*[7] Reset */KeyEvent.VK_HOME, 0,
0385: /*[8] RotateLeft */KeyEvent.VK_LEFT, KeyEvent.CTRL_MASK,
0386: /*[9] RotateRight */KeyEvent.VK_RIGHT, KeyEvent.CTRL_MASK };
0387:
0388: /**
0389: * Connstants indicating the type of action to perform: translation, zoom or rotation.
0390: */
0391: private static final short[] ACTION_TYPE = {
0392: /*[0] Left */(short) TRANSLATE_X,
0393: /*[1] Right */(short) TRANSLATE_X,
0394: /*[2] Up */(short) TRANSLATE_Y,
0395: /*[3] Down */(short) TRANSLATE_Y,
0396: /*[4] ZoomIn */(short) SCALE_X | SCALE_Y,
0397: /*[5] ZoomOut */(short) SCALE_X | SCALE_Y,
0398: /*[6] ZoomMax */(short) DEFAULT_ZOOM,
0399: /*[7] Reset */(short) RESET,
0400: /*[8] RotateLeft */(short) ROTATE,
0401: /*[9] RotateRight */(short) ROTATE };
0402:
0403: /**
0404: * Amounts by which to translate, zoom or rotate the contents of the window.
0405: */
0406: private static final double[] ACTION_AMOUNT = {
0407: /*[0] Left */+AMOUNT_TRANSLATE,
0408: /*[1] Right */-AMOUNT_TRANSLATE,
0409: /*[2] Up */+AMOUNT_TRANSLATE,
0410: /*[3] Down */-AMOUNT_TRANSLATE,
0411: /*[4] ZoomIn */AMOUNT_SCALE,
0412: /*[5] ZoomOut */1 / AMOUNT_SCALE,
0413: /*[6] ZoomMax */Double.NaN,
0414: /*[7] Reset */Double.NaN,
0415: /*[8] RotateLeft */-AMOUNT_ROTATE,
0416: /*[9] RotateRight */+AMOUNT_ROTATE };
0417:
0418: /**
0419: * List of operation types forming a group. During creation of the
0420: * menus, the different groups will be separated by a menu separator.
0421: */
0422: private static final int[] GROUP = { TRANSLATE_X | TRANSLATE_Y,
0423: SCALE_X | SCALE_Y | DEFAULT_ZOOM | RESET, ROTATE };
0424:
0425: /**
0426: * {@code ComponentUI} object in charge of obtaining the preferred
0427: * size of a {@code ZoomPane} object as well as drawing it.
0428: */
0429: private static final ComponentUI UI = new ComponentUI() {
0430: /**
0431: * Returns a default minimum size.
0432: */
0433: public Dimension getMinimumSize(final JComponent c) {
0434: return new Dimension(MINIMUM_SIZE, MINIMUM_SIZE);
0435: }
0436:
0437: /**
0438: * Returns the maximum size. We use the preferred
0439: * size as a default maximum size.
0440: */
0441: public Dimension getMaximumSize(final JComponent c) {
0442: return getPreferredSize(c);
0443: }
0444:
0445: /**
0446: * Returns the default preferred size. User can override this
0447: * preferred size by invoking {@link JComponent#setPreferredSize}.
0448: */
0449: public Dimension getPreferredSize(final JComponent c) {
0450: return ((ZoomPane) c).getDefaultSize();
0451: }
0452:
0453: /**
0454: * Override {@link ComponentUI#update} in order to handle painting of
0455: * magnifying glass, which is a special case. Since the magnifying
0456: * glass is painted just after the normal component, we don't want to
0457: * clear the background before painting it.
0458: */
0459: public void update(final Graphics g, final JComponent c) {
0460: switch (((ZoomPane) c).flag) {
0461: case IS_PAINTING_MAGNIFIER:
0462: paint(g, c);
0463: break; // Avoid background clearing
0464: default:
0465: super .update(g, c);
0466: break;
0467: }
0468: }
0469:
0470: /**
0471: * Paint the component. This method basically delegates the
0472: * work to {@link ZoomPane#paintComponent(Graphics2D)}.
0473: */
0474: public void paint(final Graphics g, final JComponent c) {
0475: final ZoomPane pane = (ZoomPane) c;
0476: final Graphics2D gr = (Graphics2D) g;
0477: switch (pane.flag) {
0478: case IS_PAINTING:
0479: pane.paintComponent(gr);
0480: break;
0481: case IS_PAINTING_MAGNIFIER:
0482: pane.paintMagnifier(gr);
0483: break;
0484: case IS_PRINTING:
0485: pane.printComponent(gr);
0486: break;
0487: default:
0488: throw new IllegalStateException(Integer
0489: .toString(pane.flag));
0490: }
0491: }
0492: };
0493:
0494: /**
0495: * Object in charge of drawing a box representing the user's selection. We
0496: * retain a reference to this object in order to be able to register it and
0497: * extract it at will from the list of objects interested in being notified
0498: * of the mouse movements.
0499: */
0500: private final MouseListener mouseSelectionTracker = new MouseSelectionTracker() {
0501: /**
0502: * Returns the selection shape. This is usually a rectangle, but could
0503: * very well be an ellipse or any other kind of geometric shape. This
0504: * method asks {@link ZoomPane#getMouseSelectionShape} for the shape.
0505: */
0506: protected Shape getModel(final MouseEvent event) {
0507: final Point2D point = new Point2D.Double(event.getX(),
0508: event.getY());
0509: if (getZoomableBounds().contains(point))
0510: try {
0511: return getMouseSelectionShape(zoom
0512: .inverseTransform(point, point));
0513: } catch (NoninvertibleTransformException exception) {
0514: unexpectedException("getModel", exception);
0515: }
0516: return null;
0517: }
0518:
0519: /**
0520: * Invoked when the user finishes the selection. This method will
0521: * delegate the action to {@link ZoomPane#mouseSelectionPerformed}.
0522: * Default implementation will perform a zoom.
0523: */
0524: protected void selectionPerformed(int ox, int oy, int px, int py) {
0525: try {
0526: final Shape selection = getSelectedArea(zoom);
0527: if (selection != null) {
0528: mouseSelectionPerformed(selection);
0529: }
0530: } catch (NoninvertibleTransformException exception) {
0531: unexpectedException("selectionPerformed", exception);
0532: }
0533: }
0534: };
0535:
0536: /**
0537: * Class responsible for listening out for the different events necessary for the smooth
0538: * working of {@link ZoomPane}. This class will listen out for mouse clicks (in order to
0539: * eventually claim the focus or make a contextual menu appear). It will listen out for
0540: * changes in the size of the component (to adjust the zoom), etc.
0541: *
0542: * @version $Id: ZoomPane.java 27862 2007-11-12 19:51:19Z desruisseaux $
0543: * @author Martin Desruisseaux
0544: */
0545: private final class Listeners extends MouseAdapter implements
0546: MouseWheelListener, ComponentListener, Serializable {
0547: public void mouseWheelMoved(final MouseWheelEvent event) {
0548: ZoomPane.this .mouseWheelMoved(event);
0549: }
0550:
0551: public void mousePressed(final MouseEvent event) {
0552: ZoomPane.this .mayShowPopupMenu(event);
0553: }
0554:
0555: public void mouseReleased(final MouseEvent event) {
0556: ZoomPane.this .mayShowPopupMenu(event);
0557: }
0558:
0559: public void componentResized(final ComponentEvent event) {
0560: ZoomPane.this .processSizeEvent(event);
0561: }
0562:
0563: public void componentMoved(final ComponentEvent event) {
0564: }
0565:
0566: public void componentShown(final ComponentEvent event) {
0567: }
0568:
0569: public void componentHidden(final ComponentEvent event) {
0570: }
0571: }
0572:
0573: /**
0574: * Affine transform containing zoom factors, translations and rotations. During the
0575: * painting of a component, this affine transform should be combined with a call to
0576: * <code>{@link Graphics2D#transform(AffineTransform) Graphics2D.transform}(zoom)</code>.
0577: */
0578: protected final AffineTransform zoom = new AffineTransform();
0579:
0580: /**
0581: * Indicates whether the zoom is the result of a {@link #reset} operation.
0582: */
0583: private boolean zoomIsReset;
0584:
0585: /**
0586: * Types of zoom permitted. This field should be a combination of the constants
0587: * {@link #SCALE_X}, {@link #SCALE_Y}, {@link #TRANSLATE_X}, {@link #TRANSLATE_Y},
0588: * {@link #ROTATE}, {@link #RESET} and {@link #DEFAULT_ZOOM}.
0589: */
0590: private final int type;
0591:
0592: /**
0593: * Strategy to follow in order to calculate the initial affine transform. The value
0594: * {@code true} indicates that the content should fill the entire panel, even if it
0595: * means losing some of the edges. The value {@code false} indicates, on the contrary,
0596: * that we should display the entire contents, even if it means leaving blank spaces in
0597: * the panel.
0598: */
0599: private boolean fillPanel = false;
0600:
0601: /**
0602: * Rectangle representing the logical coordinates of the visible region. This information is
0603: * used to keep the same region when the size or position of the component changes. Initially,
0604: * this rectangle is empty. It will only stop being empty if {@link #reset} is called and
0605: * {@link #getPreferredArea} and {@link #getZoomableBounds} have both returned valid coordinates.
0606: *
0607: * @see #getVisibleArea
0608: * @see #setVisibleArea
0609: */
0610: private final Rectangle2D visibleArea = new Rectangle2D.Double();
0611:
0612: /**
0613: * Rectangle representing the logical coordinates of the region to display initially, the first
0614: * time that the window is displayed. The value {@code null} indicates a call to {@link #getArea}.
0615: *
0616: * @see #getPreferredArea
0617: * @see #setPreferredArea
0618: */
0619: private Rectangle2D preferredArea;
0620:
0621: /**
0622: * Menu to display when the user right clicks with their mouse.
0623: * This menu will contain the navigation options.
0624: *
0625: * @see #getPopupMenu
0626: */
0627: private transient PointPopupMenu navigationPopupMenu;
0628:
0629: /**
0630: * Flag indicating which part of the paint is in progress. The permitted values are
0631: * {@link #IS_PAINTING}, {@link #IS_PAINTING_MAGNIFIER} and {@link #IS_PRINTING}.
0632: */
0633: private transient int flag;
0634:
0635: /**
0636: * Indicates if this {@code ZoomPane} object should be repainted when the user adjusts the
0637: * scrollbars. The default value is {@code false}, which means that {@code ZoomPane} will
0638: * wait until the user has released the scrollbar before repainting the component.
0639: *
0640: * @see #isPaintingWhileAdjusting
0641: * @see #setPaintingWhileAdjusting
0642: */
0643: private boolean paintingWhileAdjusting;
0644:
0645: /**
0646: * Rectangle in which to place the coordinates returned by {@link #getZoomableBounds}. This
0647: * object is defined in order to avoid allocating objects too often {@link Rectangle}.
0648: */
0649: private transient Rectangle cachedBounds;
0650:
0651: /**
0652: * Object in which to record the result of {@link #getInsets}. Used in order to avoid
0653: * {@link #getZoomableBounds} allocating {@link Insets} objects too often.
0654: */
0655: private transient Insets cachedInsets;
0656:
0657: /**
0658: * Indicates whether the user is authorised to display the magnifying glass.
0659: * The default value is {@code true}.
0660: */
0661: private boolean magnifierEnabled = true;
0662:
0663: /**
0664: * Magnification factor inside the magnifying glass. This factor must be greater than 1.
0665: */
0666: private double magnifierPower = 4;
0667:
0668: /**
0669: * Geometric shape in which to magnify. The coordinates of this shape should be expressed
0670: * in pixels. The value {@code null} means that no magnifying glass will be drawn.
0671: */
0672: private transient MouseReshapeTracker magnifier;
0673:
0674: /**
0675: * Colour with which to tint magnifying glass.
0676: */
0677: private Paint magnifierGlass = DEFAULT_MAGNIFIER_GLASS;
0678:
0679: /**
0680: * Colour of the magnifying glass's border.
0681: */
0682: private Paint magnifierBorder = DEFAULT_MAGNIFIER_BORDER;
0683:
0684: /**
0685: * Construct a {@code ZoomPane}.
0686: *
0687: * @param type Allowed zoom type. It can be a bitwise combination of the following constants:
0688: * {@link #SCALE_X}, {@link #SCALE_Y}, {@link #UNIFORM_SCALE}, {@link #TRANSLATE_X},
0689: * {@link #TRANSLATE_Y}, {@link #ROTATE}, {@link #RESET} and {@link #DEFAULT_ZOOM}.
0690: * @throws IllegalArgumentException If {@code type} is invalid.
0691: */
0692: public ZoomPane(final int type) throws IllegalArgumentException {
0693: if ((type & ~MASK) != 0) {
0694: throw new IllegalArgumentException();
0695: }
0696: this .type = type;
0697: final Vocabulary resources = Vocabulary.getResources(null);
0698: final InputMap inputMap = getInputMap();
0699: final ActionMap actionMap = getActionMap();
0700: for (int i = 0; i < ACTION_ID.length; i++) {
0701: final short actionType = ACTION_TYPE[i];
0702: if ((actionType & type) != 0) {
0703: final String actionID = ACTION_ID[i];
0704: final double amount = ACTION_AMOUNT[i];
0705: final int keyboard = ACTION_KEY[(i << 1) + 0];
0706: final int modifier = ACTION_KEY[(i << 1) + 1];
0707: final KeyStroke stroke = KeyStroke.getKeyStroke(
0708: keyboard, modifier);
0709: final Action action = new AbstractAction() {
0710: /*
0711: * Action to perform when a key has been hit or the mouse clicked.
0712: */
0713: public void actionPerformed(final ActionEvent event) {
0714: Point point = null;
0715: final Object source = event.getSource();
0716: final boolean button = (source instanceof AbstractButton);
0717: if (button) {
0718: for (Container c = (Container) source; c != null; c = c
0719: .getParent()) {
0720: if (c instanceof PointPopupMenu) {
0721: point = ((PointPopupMenu) c).point;
0722: break;
0723: }
0724: }
0725: }
0726: double m = amount;
0727: if (button
0728: || (event.getModifiers() & ActionEvent.SHIFT_MASK) != 0) {
0729: if ((actionType & UNIFORM_SCALE) != 0) {
0730: m = (m >= 1) ? 2.0 : 0.5;
0731: } else {
0732: m *= ENHANCEMENT_FACTOR;
0733: }
0734: }
0735: transform(actionType & type, m, point);
0736: }
0737: };
0738: action.putValue(Action.NAME, resources
0739: .getString(RESOURCE_ID[i]));
0740: action.putValue(Action.ACTION_COMMAND_KEY, actionID);
0741: action.putValue(Action.ACCELERATOR_KEY, stroke);
0742: actionMap.put(actionID, action);
0743: inputMap.put(stroke, actionID);
0744: inputMap.put(KeyStroke.getKeyStroke(keyboard, modifier
0745: | KeyEvent.SHIFT_MASK), actionID);
0746: }
0747: }
0748: /*
0749: * Adds an object which will be in charge of listening for mouse clicks in order to
0750: * display a contextual menu, as well as an object which will be in charge of listening
0751: * for mouse movements in order to perform zooms.
0752: */
0753: final Listeners listeners = new Listeners();
0754: addComponentListener(listeners);
0755: super .addMouseListener(listeners);
0756: if ((type & (SCALE_X | SCALE_Y)) != 0) {
0757: super .addMouseWheelListener(listeners);
0758: }
0759: super .addMouseListener(mouseSelectionTracker);
0760: setAutoscrolls(true);
0761: setFocusable(true);
0762: setOpaque(true);
0763: setUI(UI);
0764: }
0765:
0766: /**
0767: * Reinitializes the affine transform {@link #zoom} in order to cancel any zoom, rotation or
0768: * translation. The default implementation initializes the affine transform {@link #zoom} in
0769: * order to make the <var>y</var> axis point upwards and make the whole of the region covered
0770: * by the {@link #getPreferredArea} logical coordinates appear in the panel.
0771: * <p>
0772: * Note: for the derived classes: {@code reset()} is <u>the only</u> method of {@code ZoomPane}
0773: * which doesn't have to pass through {@link #transform(AffineTransform)} to modify the zoom.
0774: * This exception is necessary to avoid falling into an infinite loop.
0775: */
0776: public void reset() {
0777: reset(getZoomableBounds(), true);
0778: }
0779:
0780: /**
0781: * Reinitializes the affine transform {@link #zoom} in order to cancel any zoom, rotation or
0782: * translation. The argument {@code yAxisUpward} indicates whether the <var>y</var> axis should
0783: * point upwards. The value {@code false} lets it point downwards. This method is offered
0784: * for convenience sake for derived classes which want to redefine {@link #reset()}.
0785: *
0786: * @param zoomableBounds Coordinates, in pixels, of the screen space in which to draw.
0787: * This argument will usually be
0788: * <code>{@link #getZoomableBounds(Rectangle) getZoomableBounds}(null)</code>.
0789: * @param yAxisUpward {@code true} if the <var>y</var> axis should point upwards rather than
0790: * downwards.
0791: */
0792: protected final void reset(final Rectangle zoomableBounds,
0793: final boolean yAxisUpward) {
0794: if (!zoomableBounds.isEmpty()) {
0795: final Rectangle2D preferredArea = getPreferredArea();
0796: if (isValid(preferredArea)) {
0797: final AffineTransform change;
0798: try {
0799: change = zoom.createInverse();
0800: } catch (NoninvertibleTransformException exception) {
0801: unexpectedException("reset", exception);
0802: return;
0803: }
0804: if (yAxisUpward) {
0805: zoom.setToScale(+1, -1);
0806: } else {
0807: zoom.setToIdentity();
0808: }
0809: final AffineTransform transform = setVisibleArea(
0810: preferredArea, zoomableBounds, SCALE_X
0811: | SCALE_Y | TRANSLATE_X | TRANSLATE_Y);
0812: change.concatenate(zoom);
0813: zoom.concatenate(transform);
0814: change.concatenate(transform);
0815: getVisibleArea(zoomableBounds); // Force update of 'visibleArea'
0816: /*
0817: * The three private versions 'fireZoomPane0', 'getVisibleArea'
0818: * and 'setVisibleArea' avoid calling other methods of ZoomPane
0819: * so as not to end up in an infinite loop.
0820: */
0821: if (!change.isIdentity()) {
0822: fireZoomChanged0(change);
0823: repaint(zoomableBounds);
0824: }
0825: zoomIsReset = true;
0826: log("reset", visibleArea);
0827: }
0828: }
0829: }
0830:
0831: /**
0832: * Set the policy for the zoom when the content is initially drawn or when the user resets the
0833: * zoom. Value {@code true} means that the panel should initially be completely filled, even if
0834: * the content partially falls outside the panel's bounds. Value {@code false} means that the
0835: * full content should appear in the panel, even if some space is not used. Default value is
0836: * {@code false}.
0837: */
0838: protected void setResetPolicy(final boolean fill) {
0839: fillPanel = fill;
0840: }
0841:
0842: /**
0843: * Returns a bounding box that contains the logical coordinates of all data that may be
0844: * displayed in this {@code ZoomPane}. For example, if this {@code ZoomPane} is to display
0845: * a geographic map, then this method should return the map's bounds in degrees of latitude
0846: * and longitude. This bounding box is completely independent of any current zoom setting and
0847: * will change only if the content changes.
0848: *
0849: * @return A bounding box for the logical coordinates of all contents that are going to be
0850: * drawn in this {@code ZoomPane}. If this bounding box is unknown, then this method
0851: * can return {@code null} (but this is not recommended).
0852: */
0853: public abstract Rectangle2D getArea();
0854:
0855: /**
0856: * Indicates whether the logical coordinates of a region have been defined. This method returns
0857: * {@code true} if {@link #setPreferredArea} has been called with a non null argument.
0858: */
0859: public final boolean hasPreferredArea() {
0860: return preferredArea != null;
0861: }
0862:
0863: /**
0864: * Returns the logical coordinates of the region that we want to see displayed the first time
0865: * that {@code ZoomPane} appears on the screen. This region will also be displayed each time
0866: * the method {link #reset} is called. The default implementation goes as follows:
0867: *
0868: * <ul>
0869: * <li>If a region has already been defined by a call to
0870: * {@link #setPreferredArea}, this region will be returned.</li>
0871: * <li>If not, the whole region {@link #getArea} will be returned.</li>
0872: * </ul>
0873: *
0874: * @return The logical coordinates of the region to be initially displayed,
0875: * or {@code null} if these coordinates are unknown.
0876: */
0877: public final Rectangle2D getPreferredArea() {
0878: return (preferredArea != null) ? (Rectangle2D) preferredArea
0879: .clone() : getArea();
0880: }
0881:
0882: /**
0883: * Specifies the logical coordinates of the region that we want to see displayed the first time
0884: * that {@code ZoomPane} appears on the screen. This region will also be displayed the first
0885: * time that the method {link #reset} is called.
0886: */
0887: public final void setPreferredArea(final Rectangle2D area) {
0888: if (area != null) {
0889: if (isValid(area)) {
0890: final Object oldArea;
0891: if (preferredArea == null) {
0892: oldArea = null;
0893: preferredArea = new Rectangle2D.Double();
0894: } else
0895: oldArea = preferredArea.clone();
0896: preferredArea.setRect(area);
0897: firePropertyChange("preferredArea", oldArea, area);
0898: log("setPreferredArea", area);
0899: } else {
0900: throw new IllegalArgumentException(Errors.format(
0901: ErrorKeys.BAD_RECTANGLE_$1, area));
0902: }
0903: } else
0904: preferredArea = null;
0905: }
0906:
0907: /**
0908: * Returns the logical coordinates of the region visible on the screen. In the case of a
0909: * geographic map, for example, the logical coordinates can be expressed in degrees of
0910: * latitude/longitude or even in metres if a cartographic projection has been defined.
0911: */
0912: public final Rectangle2D getVisibleArea() {
0913: return getVisibleArea(getZoomableBounds());
0914: }
0915:
0916: /**
0917: * Implementation of {@link #getVisibleArea()}.
0918: */
0919: private Rectangle2D getVisibleArea(final Rectangle zoomableBounds) {
0920: if (zoomableBounds.isEmpty()) {
0921: return (Rectangle2D) visibleArea.clone();
0922: }
0923: Rectangle2D visible;
0924: try {
0925: visible = XAffineTransform.inverseTransform(zoom,
0926: zoomableBounds, null);
0927: } catch (NoninvertibleTransformException exception) {
0928: unexpectedException("getVisibleArea", exception);
0929: visible = new Rectangle2D.Double(zoomableBounds
0930: .getCenterX(), zoomableBounds.getCenterY(), 0, 0);
0931: }
0932: visibleArea.setRect(visible);
0933: return visible;
0934: }
0935:
0936: /**
0937: * Defines the limits of the visible part, in logical coordinates. This method will modify the
0938: * zoom and the translation in order to display the specified region. If {@link #zoom} contains
0939: * a rotation, this rotation will not be modified.
0940: *
0941: * @param logicalBounds Logical coordinates of the region to be displayed.
0942: * @throws IllegalArgumentException if {@code source} is empty.
0943: */
0944: public void setVisibleArea(final Rectangle2D logicalBounds)
0945: throws IllegalArgumentException {
0946: log("setVisibleArea", logicalBounds);
0947: transform(setVisibleArea(logicalBounds, getZoomableBounds(), 0));
0948: }
0949:
0950: /**
0951: * Defines the limits of the visible part, in logical coordinates. This method will modify the
0952: * zoom and the translation in order to display the specified region. If {@link #zoom} contains
0953: * a rotation, this rotation will not be modified.
0954: *
0955: * @param source Logical coordinates of the region to be displayed.
0956: * @param dest Pixel coordinates of the region of the window in which to
0957: * draw (normally {@link #getZoomableBounds()}).
0958: * @param mask A mask to {@code OR} with the {@link #type} for determining which
0959: * kind of transformation are allowed. The {@link #type} is not modified.
0960: * @return Change to apply to the affine transform {@link #zoom}.
0961: * @throws IllegalArgumentException if {@code source} is empty.
0962: */
0963: private AffineTransform setVisibleArea(Rectangle2D source,
0964: Rectangle2D dest, int mask) throws IllegalArgumentException {
0965: /*
0966: * Verifies the validity of the source rectangle. An invalid rectangle will be rejected.
0967: * However, we will be more flexible for dest since the window could have been reduced by
0968: * the user.
0969: */
0970: if (!isValid(source)) {
0971: throw new IllegalArgumentException(Errors.format(
0972: ErrorKeys.BAD_RECTANGLE_$1, source));
0973: }
0974: if (!isValid(dest)) {
0975: return new AffineTransform();
0976: }
0977: /*
0978: * Converts the destination into logical coordinates. We can then perform
0979: * a zoom and a translation which would put {@code source} in {@code dest}.
0980: */
0981: try {
0982: dest = XAffineTransform.inverseTransform(zoom, dest, null);
0983: } catch (NoninvertibleTransformException exception) {
0984: unexpectedException("setVisibleArea", exception);
0985: return new AffineTransform();
0986: }
0987: final double sourceWidth = source.getWidth();
0988: final double sourceHeight = source.getHeight();
0989: final double destWidth = dest.getWidth();
0990: final double destHeight = dest.getHeight();
0991: double sx = destWidth / sourceWidth;
0992: double sy = destHeight / sourceHeight;
0993: /*
0994: * Standardizes the horizontal and vertical scales,
0995: * if such a standardization has been requested.
0996: */
0997: mask |= type;
0998: if ((mask & UNIFORM_SCALE) == UNIFORM_SCALE) {
0999: if (fillPanel) {
1000: if (sy * sourceWidth > destWidth) {
1001: sx = sy;
1002: } else if (sx * sourceHeight > destHeight) {
1003: sy = sx;
1004: }
1005: } else {
1006: if (sy * sourceWidth < destWidth) {
1007: sx = sy;
1008: } else if (sx * sourceHeight < destHeight) {
1009: sy = sx;
1010: }
1011: }
1012: }
1013: final AffineTransform change = AffineTransform
1014: .getTranslateInstance((mask & TRANSLATE_X) != 0 ? dest
1015: .getCenterX() : 0,
1016: (mask & TRANSLATE_Y) != 0 ? dest.getCenterY()
1017: : 0);
1018: change.scale((mask & SCALE_X) != 0 ? sx : 1,
1019: (mask & SCALE_Y) != 0 ? sy : 1);
1020: change.translate((mask & TRANSLATE_X) != 0 ? -source
1021: .getCenterX() : 0, (mask & TRANSLATE_Y) != 0 ? -source
1022: .getCenterY() : 0);
1023: XAffineTransform.round(change);
1024: return change;
1025: }
1026:
1027: /**
1028: * Returns the bounding box (in pixel coordinates) of the zoomable area.
1029: * <strong>For performance reasons, this method reuses an internal cache.
1030: * Never modify the returned rectangle!</strong>. This internal method
1031: * is invoked by every method looking for this {@code ZoomPane}
1032: * dimension.
1033: *
1034: * @return The bounding box of the zoomable area, in pixel coordinates
1035: * relative to this {@code ZoomPane} widget. <strong>Do not
1036: * change the returned rectangle!</strong>
1037: */
1038: private final Rectangle getZoomableBounds() {
1039: return cachedBounds = getZoomableBounds(cachedBounds);
1040: }
1041:
1042: /**
1043: * Returns the bounding box (in pixel coordinates) of the zoomable area. This method is similar
1044: * to {@link #getBounds(Rectangle)}, except that the zoomable area may be smaller than the whole
1045: * widget area. For example, a chart needs to keep some space for axes around the zoomable area.
1046: * Another difference is that pixel coordinates are relative to the widget, i.e. the (0,0)
1047: * coordinate lies on the {@code ZoomPane} upper left corner, no matter what its location on
1048: * screen.
1049: * <p>
1050: * {@code ZoomPane} invokes {@code getZoomableBounds} when it needs to set up an initial
1051: * {@link #zoom} value. Subclasses should also set the clip area to this bounding box in their
1052: * {@link #paintComponent(Graphics2D)} method <em>before</em> setting the graphics transform.
1053: * For example:
1054: *
1055: * <blockquote><pre>
1056: * graphics.clip(getZoomableBounds(null));
1057: * graphics.transform({@link #zoom});
1058: * </pre></blockquote>
1059: *
1060: * @param bounds An optional pre-allocated rectangle, or {@code null} to create a new one. This
1061: * argument is useful if the caller wants to avoid allocating a new object on the heap.
1062: * @return The bounding box of the zoomable area, in pixel coordinates
1063: * relative to this {@code ZoomPane} widget.
1064: */
1065: protected Rectangle getZoomableBounds(Rectangle bounds) {
1066: Insets insets;
1067: bounds = getBounds(bounds);
1068: insets = cachedInsets;
1069: insets = getInsets(insets);
1070: cachedInsets = insets;
1071: if (bounds.isEmpty()) {
1072: final Dimension size = getPreferredSize();
1073: bounds.width = size.width;
1074: bounds.height = size.height;
1075: }
1076: bounds.x = insets.left;
1077: bounds.y = insets.top;
1078: bounds.width -= (insets.left + insets.right);
1079: bounds.height -= (insets.top + insets.bottom);
1080: return bounds;
1081: }
1082:
1083: /**
1084: * Returns the default size for this component. This is the size returned by
1085: * {@link #getPreferredSize} if no preferred size has been explicitly set with
1086: * {@link #setPreferredSize}.
1087: *
1088: * @return The default size for this component.
1089: */
1090: protected Dimension getDefaultSize() {
1091: return getViewSize();
1092: }
1093:
1094: /**
1095: * Returns the preferred pixel size for a close zoom. For image rendering, the preferred pixel
1096: * size is the image's pixel size in logical units. For other kinds of rendering, this "pixel"
1097: * size should be some reasonable resolution. The default implementation computes a default
1098: * value from {@link #getArea}.
1099: */
1100: protected Dimension2D getPreferredPixelSize() {
1101: final Rectangle2D area = getArea();
1102: if (isValid(area)) {
1103: return new XDimension2D.Double(area.getWidth()
1104: / (10 * getWidth()), area.getHeight()
1105: / (10 * getHeight()));
1106: } else {
1107: return new Dimension(1, 1);
1108: }
1109: }
1110:
1111: /**
1112: * Returns the current {@linkplain #zoom} scale factor. A value of 1/100 means that 100 metres
1113: * are displayed as 1 pixel (provided that the logical coordinates of {@link #getArea} are
1114: * expressed in metres). Scale factors for X and Y axes can be computed separately using the
1115: * following equations:
1116: *
1117: * <table cellspacing=3><tr>
1118: * <td width=50%><IMG src="doc-files/scaleX.png"></td>
1119: * <td width=50%><IMG src="doc-files/scaleY.png"></td>
1120: * </tr></table>
1121: *
1122: * This method combines scale along both axes, which is correct if this {@code ZoomPane} has
1123: * been constructed with the {@link #UNIFORM_SCALE} type.
1124: */
1125: public double getScaleFactor() {
1126: final double m00 = zoom.getScaleX();
1127: final double m11 = zoom.getScaleY();
1128: final double m01 = zoom.getShearX();
1129: final double m10 = zoom.getShearY();
1130: return Math.sqrt(m00 * m00 + m11 * m11 + m01 * m01 + m10 * m10);
1131: }
1132:
1133: /**
1134: * Changes the {@linkplain #zoom} by applying an affine transform. The {@code change} transform
1135: * must express a change in logical units, for example, a translation in metres. This method is
1136: * conceptually similar to the following code:
1137: *
1138: * <pre>
1139: * {@link #zoom}.{@link AffineTransform#concatenate(AffineTransform) concatenate}(change);
1140: * {@link #fireZoomChanged(AffineTransform) fireZoomChanged}(change);
1141: * {@link #repaint() repaint}({@link #getZoomableBounds getZoomableBounds}(null));
1142: * </pre>
1143: *
1144: * @param change The zoom change, as an affine transform in logical coordinates. If
1145: * {@code change} is the identity transform, then this method does nothing and
1146: * listeners are not notified.
1147: */
1148: public void transform(final AffineTransform change) {
1149: if (!change.isIdentity()) {
1150: zoom.concatenate(change);
1151: XAffineTransform.round(zoom);
1152: fireZoomChanged(change);
1153: repaint(getZoomableBounds());
1154: zoomIsReset = false;
1155: }
1156: }
1157:
1158: /**
1159: * Changes the {@linkplain #zoom} by applying an affine transform. The {@code change} transform
1160: * must express a change in pixel units, for example, a scrolling of 6 pixels toward right. This
1161: * method is conceptually similar to the following code:
1162: *
1163: * <pre>
1164: * {@link #zoom}.{@link AffineTransform#preConcatenate(AffineTransform) preConcatenate}(change);
1165: * {@link #fireZoomChanged(AffineTransform) fireZoomChanged}(<cite>change translated in logical units</cite>);
1166: * {@link #repaint() repaint}({@link #getZoomableBounds getZoomableBounds}(null));
1167: * </pre>
1168: *
1169: * @param change The zoom change, as an affine transform in pixel coordinates. If
1170: * {@code change} is the identity transform, then this method does nothing
1171: * and listeners are not notified.
1172: *
1173: * @since 2.1
1174: */
1175: public void transformPixels(final AffineTransform change) {
1176: if (!change.isIdentity()) {
1177: final AffineTransform logical;
1178: try {
1179: logical = zoom.createInverse();
1180: } catch (NoninvertibleTransformException exception) {
1181: // TODO: uncomment the argument when we will allowed to compile for J2SE 1.5
1182: throw new IllegalStateException(/*exception*/);
1183: }
1184: logical.concatenate(change);
1185: logical.concatenate(zoom);
1186: XAffineTransform.round(logical);
1187: transform(logical);
1188: }
1189: }
1190:
1191: /**
1192: * Carries out a zoom, a translation or a rotation on the contents of {@code ZoomPane}. The
1193: * type of operation to carry out depends on the {@code operation} argument:
1194: *
1195: * <ul>
1196: * <li>{@link #TRANSLATE_X} carries out a translation along the <var>x</var> axis.
1197: * The {@code amount} argument specifies the transformation to perform in number
1198: * of pixels. A negative value moves to the left whilst a positive value moves to
1199: * the right.</li>
1200: * <li>{@link #TRANSLATE_Y} carries out a translation along the <var>y</var> axis. The
1201: * {@code amount} argument specifies the transformation to perform in number of pixels.
1202: * A negative valuemoves upwards whilst a positive value moves downwards.</li>
1203: * <li>{@link #UNIFORM_SCALE} carries out a zoom. The {@code amount} argument specifies the
1204: * type of zoom to perform. A value greater than 1 will perform a zoom in whilst a value
1205: * between 0 and 1 will perform a zoom out.</li>
1206: * <li>{@link #ROTATE} carries out a rotation. The {@code amount} argument specifies the
1207: * rotation angle in radians.</li>
1208: * <li>{@link #RESET} Redefines the zoom to a default scale, rotation and translation. This
1209: * operation displays all, or almost all, the contents of {@code ZoomPane}.</li>
1210: * <li>{@link #DEFAULT_ZOOM} Carries out a default zoom, close to the maximum zoom, which
1211: * shows the details of the contents of {@code ZoomPane} but without enlarging them too
1212: * much.</li>
1213: * </ul>
1214: *
1215: * @param operation Type of operation to perform.
1216: * @param amount ({@link #TRANSLATE_X} and {@link #TRANSLATE_Y}) translation in pixels,
1217: * ({@link #SCALE_X} and {@link #SCALE_Y}) scale factor or ({@link #ROTATE}) rotation
1218: * angle in radians. In other cases, this argument is ignored and can be {@link Double#NaN}.
1219: * @param center Zoom centre ({@link #SCALE_X} and {@link #SCALE_Y}) or rotation centre
1220: * ({@link #ROTATE}), in pixel coordinates. The value {@code null} indicates a default
1221: * value, more often not the centre of the window.
1222: * @throws UnsupportedOperationException if the {@code operation} argument isn't recognized.
1223: */
1224: private void transform(final int operation, final double amount,
1225: final Point2D center) throws UnsupportedOperationException {
1226: if ((operation & (RESET)) != 0) {
1227: /////////////////////
1228: //// RESET ////
1229: /////////////////////
1230: if ((operation & ~(RESET)) != 0) {
1231: throw new UnsupportedOperationException();
1232: }
1233: reset();
1234: return;
1235: }
1236: final AffineTransform change;
1237: try {
1238: change = zoom.createInverse();
1239: } catch (NoninvertibleTransformException exception) {
1240: unexpectedException("transform", exception);
1241: return;
1242: }
1243: if ((operation & (TRANSLATE_X | TRANSLATE_Y)) != 0) {
1244: /////////////////////////
1245: //// TRANSLATE ////
1246: /////////////////////////
1247: if ((operation & ~(TRANSLATE_X | TRANSLATE_Y)) != 0) {
1248: throw new UnsupportedOperationException();
1249: }
1250: change.translate(((operation & TRANSLATE_X) != 0) ? amount
1251: : 0, ((operation & TRANSLATE_Y) != 0) ? amount : 0);
1252: } else {
1253: /*
1254: * Obtains the coordinates (in pixels) of the rotation or zoom centre.
1255: */
1256: final double centerX;
1257: final double centerY;
1258: if (center != null) {
1259: centerX = center.getX();
1260: centerY = center.getY();
1261: } else {
1262: final Rectangle bounds = getZoomableBounds();
1263: if (bounds.width >= 0 && bounds.height >= 0) {
1264: centerX = bounds.getCenterX();
1265: centerY = bounds.getCenterY();
1266: } else {
1267: return;
1268: }
1269: /*
1270: * Zero lengths and widths are accepted. If, however, the rectangle isn't valid
1271: * (negative length or width) then the method will end without doing anything. No
1272: * zoom will be performed.
1273: */
1274: }
1275: if ((operation & (ROTATE)) != 0) {
1276: //////////////////////
1277: //// ROTATE ////
1278: //////////////////////
1279: if ((operation & ~(ROTATE)) != 0) {
1280: throw new UnsupportedOperationException();
1281: }
1282: change.rotate(amount, centerX, centerY);
1283: } else if ((operation & (SCALE_X | SCALE_Y)) != 0) {
1284: /////////////////////
1285: //// SCALE ////
1286: /////////////////////
1287: if ((operation & ~(UNIFORM_SCALE)) != 0) {
1288: throw new UnsupportedOperationException();
1289: }
1290: change.translate(+centerX, +centerY);
1291: change.scale(((operation & SCALE_X) != 0) ? amount : 1,
1292: ((operation & SCALE_Y) != 0) ? amount : 1);
1293: change.translate(-centerX, -centerY);
1294: } else if ((operation & (DEFAULT_ZOOM)) != 0) {
1295: ////////////////////////////
1296: //// DEFAULT_ZOOM ////
1297: ////////////////////////////
1298: if ((operation & ~(DEFAULT_ZOOM)) != 0) {
1299: throw new UnsupportedOperationException();
1300: }
1301: final Dimension2D size = getPreferredPixelSize();
1302: double sx = 1 / (size.getWidth() * XAffineTransform
1303: .getScaleX0(zoom));
1304: double sy = 1 / (size.getHeight() * XAffineTransform
1305: .getScaleY0(zoom));
1306: if ((type & UNIFORM_SCALE) == UNIFORM_SCALE) {
1307: if (sx > sy)
1308: sx = sy;
1309: if (sy > sx)
1310: sy = sx;
1311: }
1312: if ((type & SCALE_X) == 0)
1313: sx = 1;
1314: if ((type & SCALE_Y) == 0)
1315: sy = 1;
1316: change.translate(+centerX, +centerY);
1317: change.scale(sx, sy);
1318: change.translate(-centerX, -centerY);
1319: } else {
1320: throw new UnsupportedOperationException();
1321: }
1322: }
1323: change.concatenate(zoom);
1324: XAffineTransform.round(change);
1325: transform(change);
1326: }
1327:
1328: /**
1329: * Adds an object to the list of objects interested in being notified about zoom changes.
1330: */
1331: public void addZoomChangeListener(final ZoomChangeListener listener) {
1332: listenerList.add(ZoomChangeListener.class, listener);
1333: }
1334:
1335: /**
1336: * Removes an object from the list of objects interested in being notified about zoom changes.
1337: */
1338: public void removeZoomChangeListener(
1339: final ZoomChangeListener listener) {
1340: listenerList.remove(ZoomChangeListener.class, listener);
1341: }
1342:
1343: /**
1344: * Adds an object to the list of objects interested in being notified about mouse events.
1345: */
1346: public void addMouseListener(final MouseListener listener) {
1347: super .removeMouseListener(mouseSelectionTracker);
1348: super .addMouseListener(listener);
1349: super .addMouseListener(mouseSelectionTracker); // MUST be last!
1350: }
1351:
1352: /**
1353: * Signals that a zoom change has taken place. Every object registered by the
1354: * {@link #addZoomChangeListener} method will be notified of the change as soon as possible.
1355: *
1356: * @param change Affine transform which represents the change in the zoom. That is
1357: * {@code oldZoom} and {@code newZoom} are the affine transforms of the old and new zoom
1358: * respectively. Therefore, the relation
1359: * <code>newZoom=oldZoom.{@link AffineTransform#concatenate concatenate}(change)</code>
1360: * must be respected (to within rounding errors). <strong>Note: This method can modify
1361: * {@code change}</strong> to combine several consecutive calls of {@code fireZoomChanged}
1362: * in a single transformation.
1363: */
1364: protected void fireZoomChanged(final AffineTransform change) {
1365: visibleArea.setRect(getVisibleArea());
1366: fireZoomChanged0(change);
1367: }
1368:
1369: /**
1370: * Notifies derived classes that the zoom has changed. Unlike the protected
1371: * {@link #fireZoomChanged} method, this private method doesn't modify any internal field and
1372: * doesn't attempt to call other {@code ZoomPane} methods such as {@link #getVisibleArea}. An
1373: * infinite loop is thereby avoided as this method is called by {@link #reset}.
1374: */
1375: private void fireZoomChanged0(final AffineTransform change) {
1376: /*
1377: * Note: the event must be fired even if the transformation is the identity matrix,
1378: * because certain classes use this to update scrollbars.
1379: */
1380: if (change == null) {
1381: throw new NullPointerException();
1382: }
1383: ZoomChangeEvent event = null;
1384: final Object[] listeners = listenerList.getListenerList();
1385: for (int i = listeners.length; (i -= 2) >= 0;) {
1386: if (listeners[i] == ZoomChangeListener.class) {
1387: if (event == null) {
1388: event = new ZoomChangeEvent(this , change);
1389: }
1390: try {
1391: ((ZoomChangeListener) listeners[i + 1])
1392: .zoomChanged(event);
1393: } catch (RuntimeException exception) {
1394: unexpectedException("fireZoomChanged", exception);
1395: }
1396: }
1397: }
1398: }
1399:
1400: /**
1401: * Method called automatically after the user selects an area with the mouse. The default
1402: * implementation zooms to the selected {@code area}. Derived classes can redefine this method
1403: * in order to carry out another action.
1404: *
1405: * @param area Area selected by the user, in logical coordinates.
1406: */
1407: protected void mouseSelectionPerformed(final Shape area) {
1408: final Rectangle2D rect = (area instanceof Rectangle2D) ? (Rectangle2D) area
1409: : area.getBounds2D();
1410: if (isValid(rect)) {
1411: setVisibleArea(rect);
1412: }
1413: }
1414:
1415: /**
1416: * Returns the geometric shape to be used to delimitate an area. This shape is generally a
1417: * rectangle but could also be an ellipse, an arrow or another shape. The coordinates of the
1418: * returned shape won't be taken into account. In fact, these coordinates will often be
1419: * destroyed. The only things which count are the class of the returned shape (e.g.
1420: * {@link java.awt.geom.Ellipse2D} vs {@link java.awt.geom.Rectangle2D}) and any of its
1421: * parameters not related to its position (e.g. corner rounding in a rectangle
1422: * {@link java.awt.geom.RoundRectangle2D}).
1423: * <p>
1424: * The returned shape will generally be from a class derived from {@link RectangularShape},
1425: * but can also be from the class {@link Line2D}. <strong>Any other class risks firing a
1426: * {@link ClassCastException} at execution</strong>.
1427: *
1428: * The default implementation always returns a {@link java.awt.geom.Rectangle2D} object.
1429: *
1430: * @param point Logical coordinates of the mouse at the moment the button is pressed. This
1431: * information can be used by derived classes that wish to consider the mouse position
1432: * before choosing a geometric shape.
1433: * @return Shape from the class {link RectangularShape} or {link Line2D}, or {@code null} to
1434: * indicate that we do not want to select with the mouse.
1435: */
1436: protected Shape getMouseSelectionShape(final Point2D point) {
1437: return new Rectangle2D.Float();
1438: }
1439:
1440: /**
1441: * Indicates whether or not the magnifying glass is allowed to be
1442: * displayed on this component. By default, it is allowed.
1443: */
1444: public boolean isMagnifierEnabled() {
1445: return magnifierEnabled;
1446: }
1447:
1448: /**
1449: * Specifies whether or not the magnifying glass is allowed to be displayed on this component.
1450: * Calling this method with the value {@code false} will hide the magnifying glass, delete the
1451: * choice "Display magnifying glass" from the contextual menu and lead to all calls to
1452: * <code>{@link #setMagnifierVisible setMagnifierVisible}(true)</code> being ignored.
1453: */
1454: public void setMagnifierEnabled(final boolean enabled) {
1455: magnifierEnabled = enabled;
1456: navigationPopupMenu = null;
1457: if (!enabled) {
1458: setMagnifierVisible(false);
1459: }
1460: }
1461:
1462: /**
1463: * Indicates whether or not the magnifying glass is visible. By default, it is not visible.
1464: * Call {@link #setMagnifierVisible(boolean)} to make it appear.
1465: */
1466: public boolean isMagnifierVisible() {
1467: return magnifier != null;
1468: }
1469:
1470: /**
1471: * Displays or hides the magnifying glass. If the magnifying glass is not visible and this
1472: * method is called with the argument {@code true}, the magnifying glass will appear at the
1473: * centre of the window.
1474: */
1475: public void setMagnifierVisible(final boolean visible) {
1476: setMagnifierVisible(visible, null);
1477: }
1478:
1479: /**
1480: * Returns the color with which to tint magnifying glass.
1481: */
1482: public Paint getMagnifierGlass() {
1483: return magnifierGlass;
1484: }
1485:
1486: /**
1487: * Set the color with which to tint magnifying glass.
1488: */
1489: public void setMagnifierGlass(final Paint color) {
1490: final Paint old = magnifierGlass;
1491: magnifierGlass = color;
1492: firePropertyChange("magnifierGlass", old, color);
1493: }
1494:
1495: /**
1496: * Returns the color of the magnifying glass's border.
1497: */
1498: public Paint getMagnifierBorder() {
1499: return magnifierBorder;
1500: }
1501:
1502: /**
1503: * Set the color of the magnifying glass's border.
1504: */
1505: public void setMagnifierBorder(final Paint color) {
1506: final Paint old = magnifierBorder;
1507: magnifierBorder = color;
1508: firePropertyChange("magnifierBorder", old, color);
1509: }
1510:
1511: /**
1512: * Corrects a pixel's coordinates for removing the effect of the magnifying glass. Without this
1513: * method, transformations from pixels to geographic coordinates would not give accurate results
1514: * for pixels inside the magnifier since the magnifier moves the pixel's apparent position.
1515: * Invoking this method will remove deformation effects using the following steps:
1516: * <p>
1517: * <ul>
1518: * <li>If the pixel's coordinate {@code point} is outside the magnifier, then this method do
1519: * nothing.</li>
1520: * <li>Otherwise, if the pixel's coordinate is inside the magnifier, then this method update
1521: * {@code point} in such a way that it contains the position that the exact same pixel
1522: * would have in the absence of magnifier.</li>
1523: * </ul>
1524: *
1525: * @param point In input, a pixel's coordinate as it appears on the screen. In output, the
1526: * coordinate that the same pixel would have if the magnifier wasn't presents.
1527: */
1528: public final void correctApparentPixelPosition(final Point2D point) {
1529: if (magnifier != null && magnifier.contains(point)) {
1530: final double centerX = magnifier.getCenterX();
1531: final double centerY = magnifier.getCenterY();
1532: /*
1533: * The following code is equivalent to the following
1534: * transformations.
1535: * These transformations must be identical to those which
1536: * are applied in {@link #paintMagnifier}.
1537: *
1538: * translate(+centerX, +centerY);
1539: * scale (magnifierPower, magnifierPower);
1540: * translate(-centerX, -centerY);
1541: * inverseTransform(point, point);
1542: */
1543: point.setLocation((point.getX() - centerX) / magnifierPower
1544: + centerX, (point.getY() - centerY)
1545: / magnifierPower + centerY);
1546: }
1547: }
1548:
1549: /**
1550: * Displays or hides the magnifying glass. If the magnifying glass isn't visible and this
1551: * method is called with the argument {@code true}, the magnifying glass will be displayed
1552: * centred on the specified coordinate.
1553: *
1554: * @param visible {@code true} to display the magnifying glass or {@code false} to hide it.
1555: * @param center Central coordinate on which to display the magnifying glass. If the
1556: * magnifying glass was initially invisible, it will appear centred on this coordinate
1557: * (or in the centre of the screen if {@code center} is null). If the magnifying glass
1558: * was already visible and {@code center} is not null, it will be moved to centre it on
1559: * the specified coordinate.
1560: */
1561: private void setMagnifierVisible(final boolean visible,
1562: final Point center) {
1563: if (visible && magnifierEnabled) {
1564: if (magnifier == null) {
1565: Rectangle bounds = getZoomableBounds(); // Do not modify the Rectangle!
1566: if (bounds.isEmpty())
1567: bounds = new Rectangle(0, 0, DEFAULT_SIZE,
1568: DEFAULT_SIZE);
1569: final int size = Math.min(Math.min(bounds.width,
1570: bounds.height), DEFAULT_MAGNIFIER_SIZE);
1571: final int centerX, centerY;
1572: if (center != null) {
1573: centerX = center.x - size / 2;
1574: centerY = center.y - size / 2;
1575: } else {
1576: centerX = bounds.x + (bounds.width - size) / 2;
1577: centerY = bounds.y + (bounds.height - size) / 2;
1578: }
1579: magnifier = new MouseReshapeTracker(
1580: new Ellipse2D.Float(centerX, centerY, size,
1581: size)) {
1582: protected void stateWillChange(
1583: final boolean isAdjusting) {
1584: repaintMagnifier();
1585: }
1586:
1587: protected void stateChanged(
1588: final boolean isAdjusting) {
1589: repaintMagnifier();
1590: }
1591: };
1592: magnifier.setClip(bounds);
1593: magnifier.setAdjustable(SwingConstants.NORTH, true);
1594: magnifier.setAdjustable(SwingConstants.SOUTH, true);
1595: magnifier.setAdjustable(SwingConstants.EAST, true);
1596: magnifier.setAdjustable(SwingConstants.WEST, true);
1597:
1598: addMouseListener(magnifier);
1599: addMouseMotionListener(magnifier);
1600: firePropertyChange("magnifierVisible", Boolean.FALSE,
1601: Boolean.TRUE);
1602: repaintMagnifier();
1603: } else if (center != null) {
1604: final Rectangle2D frame = magnifier.getFrame();
1605: final double width = frame.getWidth();
1606: final double height = frame.getHeight();
1607: magnifier.setFrame(center.x - 0.5 * width, center.y
1608: - 0.5 * height, width, height);
1609: }
1610: } else if (magnifier != null) {
1611: repaintMagnifier();
1612: removeMouseMotionListener(magnifier);
1613: removeMouseListener(magnifier);
1614: setCursor(null);
1615: magnifier = null;
1616: firePropertyChange("magnifierVisible", Boolean.TRUE,
1617: Boolean.FALSE);
1618: }
1619: }
1620:
1621: /**
1622: * Adds navigation options to the specified menu. Menus such as "Zoom in" and "Zoom out" will
1623: * be automatically added to the menu together with the appropriate short-cut keys.
1624: */
1625: public void buildNavigationMenu(final JMenu menu) {
1626: buildNavigationMenu(menu, null);
1627: }
1628:
1629: /**
1630: * Adds navigation options to the specified menu. Menus such as "Zoom in" and "Zoom out" will
1631: * be automatically added to the menu together with the appropriate short-cut keys.
1632: */
1633: private void buildNavigationMenu(final JMenu menu,
1634: final JPopupMenu popup) {
1635: int groupIndex = 0;
1636: boolean firstMenu = true;
1637: final ActionMap actionMap = getActionMap();
1638: for (int i = 0; i < ACTION_ID.length; i++) {
1639: final Action action = actionMap.get(ACTION_ID[i]);
1640: if (action != null && action.getValue(Action.NAME) != null) {
1641: /*
1642: * Checks whether the next item belongs to a new group.
1643: * If this is the case, it will be necessary to add a separator
1644: * before the next menu.
1645: */
1646: final int lastGroupIndex = groupIndex;
1647: while ((ACTION_TYPE[i] & GROUP[groupIndex]) == 0) {
1648: groupIndex = (groupIndex + 1) % GROUP.length;
1649: if (groupIndex == lastGroupIndex) {
1650: break;
1651: }
1652: }
1653: /*
1654: * Adds an item to the menu.
1655: */
1656: if (menu != null) {
1657: if (groupIndex != lastGroupIndex && !firstMenu) {
1658: menu.addSeparator();
1659: }
1660: final JMenuItem item = new JMenuItem(action);
1661: item.setAccelerator((KeyStroke) action
1662: .getValue(Action.ACCELERATOR_KEY));
1663: menu.add(item);
1664: }
1665: if (popup != null) {
1666: if (groupIndex != lastGroupIndex && !firstMenu) {
1667: popup.addSeparator();
1668: }
1669: final JMenuItem item = new JMenuItem(action);
1670: item.setAccelerator((KeyStroke) action
1671: .getValue(Action.ACCELERATOR_KEY));
1672: popup.add(item);
1673: }
1674: firstMenu = false;
1675: }
1676: }
1677: }
1678:
1679: /**
1680: * Menu with a position. This class retains the exact coordinates of the
1681: * place the user clicked when this menu was invoked.
1682: *
1683: * @author Martin Desruisseaux
1684: * @version $Id: ZoomPane.java 27862 2007-11-12 19:51:19Z desruisseaux $
1685: */
1686: private static final class PointPopupMenu extends JPopupMenu {
1687: /**
1688: * Coordinates of the point the user clicked on.
1689: */
1690: public final Point point;
1691:
1692: /**
1693: * Constructs a menu, retaining the specified coordinate.
1694: */
1695: public PointPopupMenu(final Point point) {
1696: this .point = point;
1697: }
1698: }
1699:
1700: /**
1701: * Method called automatically when the user clicks on the right mouse button. The default
1702: * implementation displays a contextual menu containing navigation options.
1703: *
1704: * @param event Mouse event. This object contains the mouse coordinates
1705: * in geographic coordinates (as well as pixel coordinates).
1706: * @return The contextual menu, or {@code null} to avoid displaying the menu.
1707: */
1708: protected JPopupMenu getPopupMenu(final MouseEvent event) {
1709: if (getZoomableBounds().contains(event.getX(), event.getY())) {
1710: if (navigationPopupMenu == null) {
1711: navigationPopupMenu = new PointPopupMenu(event
1712: .getPoint());
1713: if (magnifierEnabled) {
1714: final Vocabulary resources = Vocabulary
1715: .getResources(getLocale());
1716: final JMenuItem item = new JMenuItem(resources
1717: .getString(VocabularyKeys.SHOW_MAGNIFIER));
1718: item.addActionListener(new ActionListener() {
1719: public void actionPerformed(
1720: final ActionEvent event) {
1721: setMagnifierVisible(true,
1722: navigationPopupMenu.point);
1723: }
1724: });
1725: navigationPopupMenu.add(item);
1726: navigationPopupMenu.addSeparator();
1727: }
1728: buildNavigationMenu(null, navigationPopupMenu);
1729: } else {
1730: navigationPopupMenu.point.x = event.getX();
1731: navigationPopupMenu.point.y = event.getY();
1732: }
1733: return navigationPopupMenu;
1734: } else
1735: return null;
1736: }
1737:
1738: /**
1739: * Method called automatically when the user clicks on the right mouse
1740: * button inside the magnifying glass. The default implementation displays
1741: * a contextual menu which contains magnifying glass options.
1742: *
1743: * @param event Mouse event containing amongst others, the mouse position.
1744: * @return The contextual menu, or {@code null} to avoid displaying the menu.
1745: */
1746: protected JPopupMenu getMagnifierMenu(final MouseEvent event) {
1747: final Vocabulary resources = Vocabulary
1748: .getResources(getLocale());
1749: final JPopupMenu menu = new JPopupMenu(resources
1750: .getString(VocabularyKeys.MAGNIFIER));
1751: final JMenuItem item = new JMenuItem(resources
1752: .getString(VocabularyKeys.HIDE));
1753: item.addActionListener(new ActionListener() {
1754: public void actionPerformed(final ActionEvent event) {
1755: setMagnifierVisible(false);
1756: }
1757: });
1758: menu.add(item);
1759: return menu;
1760: }
1761:
1762: /**
1763: * Displays the navigation contextual menu, provided the mouse event is
1764: * in fact the one which normally displays this menu.
1765: */
1766: private void mayShowPopupMenu(final MouseEvent event) {
1767: if (event.getID() == MouseEvent.MOUSE_PRESSED
1768: && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
1769: requestFocus();
1770: }
1771: if (event.isPopupTrigger()) {
1772: final Point point = event.getPoint();
1773: final JPopupMenu popup = (magnifier != null && magnifier
1774: .contains(point)) ? getMagnifierMenu(event)
1775: : getPopupMenu(event);
1776: if (popup != null) {
1777: final Component source = event.getComponent();
1778: final Window window = SwingUtilities
1779: .getWindowAncestor(source);
1780: if (window != null) {
1781: final Toolkit toolkit = source.getToolkit();
1782: final Insets insets = toolkit
1783: .getScreenInsets(window
1784: .getGraphicsConfiguration());
1785: final Dimension screen = toolkit.getScreenSize();
1786: final Dimension size = popup.getPreferredSize();
1787: SwingUtilities.convertPointToScreen(point, source);
1788: screen.width -= (size.width + insets.right);
1789: screen.height -= (size.height + insets.bottom);
1790: if (point.x > screen.width) {
1791: point.x = screen.width;
1792: }
1793: if (point.y > screen.height) {
1794: point.y = screen.height;
1795: }
1796: if (point.x < insets.left) {
1797: point.x = insets.left;
1798: }
1799: if (point.y < insets.top) {
1800: point.y = insets.top;
1801: }
1802: SwingUtilities
1803: .convertPointFromScreen(point, source);
1804: popup.show(source, point.x, point.y);
1805: }
1806: }
1807: }
1808: }
1809:
1810: /**
1811: * Method called automatically when user moves the mouse wheel. This method
1812: * performs a zoom centred on the mouse position.
1813: */
1814: private final void mouseWheelMoved(final MouseWheelEvent event) {
1815: if (event.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
1816: int rotation = event.getUnitsToScroll();
1817: double scale = 1 + (AMOUNT_SCALE - 1) * Math.abs(rotation);
1818: Point2D point = new Point2D.Double(event.getX(), event
1819: .getY());
1820: if (rotation > 0) {
1821: scale = 1 / scale;
1822: }
1823: if (magnifier != null && magnifier.contains(point)) {
1824: magnifierPower *= scale;
1825: repaintMagnifier();
1826: } else {
1827: correctApparentPixelPosition(point);
1828: transform(UNIFORM_SCALE & type, scale, point);
1829: }
1830: event.consume();
1831: }
1832: }
1833:
1834: /**
1835: * Method called each time the size or the position of the component changes.
1836: */
1837: private final void processSizeEvent(final ComponentEvent event) {
1838: if (!isValid(visibleArea) || zoomIsReset) {
1839: reset();
1840: }
1841: if (magnifier != null) {
1842: magnifier.setClip(getZoomableBounds());
1843: }
1844: /*
1845: * {@link #repaint} isn't called because there is already a {@link #repaint} command in
1846: * the queue. Therefore, the redraw will be twice as quick under JDK 1.3.
1847: * {@link #transform} isn't called either because the zoom hasn't really changed;
1848: * we have simply discovered a part of the window which was hidden before. However,
1849: * we still need to adjust the scrollbars.
1850: */
1851: final Object[] listeners = listenerList.getListenerList();
1852: for (int i = listeners.length; (i -= 2) >= 0;) {
1853: if (listeners[i] == ZoomChangeListener.class) {
1854: if (listeners[i + 1] instanceof Synchronizer)
1855: try {
1856: ((ZoomChangeListener) listeners[i + 1])
1857: .zoomChanged(null);
1858: } catch (RuntimeException exception) {
1859: unexpectedException("processSizeEvent",
1860: exception);
1861: }
1862: }
1863: }
1864: }
1865:
1866: /**
1867: * Returns an object which displays this {@code ZoomPane} with the scrollbars.
1868: */
1869: public JComponent createScrollPane() {
1870: return new ScrollPane();
1871: }
1872:
1873: /**
1874: * The scroll panel for {@link ZoomPane}. The standard {@link JScrollPane}
1875: * class is not used because it is difficult to get {@link JViewport} to
1876: * cooperate with transformations already handled by {@link ZoomPane#zoom}.
1877: *
1878: * @version $Id: ZoomPane.java 27862 2007-11-12 19:51:19Z desruisseaux $
1879: * @author Martin Desruisseaux
1880: */
1881: private final class ScrollPane extends JComponent implements
1882: PropertyChangeListener {
1883: /**
1884: * The horizontal scrollbar, or {@code null} if none.
1885: */
1886: private final JScrollBar scrollbarX;
1887:
1888: /**
1889: * The vertical scrollbar, or {@code null} if none.
1890: */
1891: private final JScrollBar scrollbarY;
1892:
1893: /**
1894: * Constructs a scroll pane for the enclosing {@link ZoomPane}.
1895: */
1896: public ScrollPane() {
1897: setOpaque(false);
1898: setLayout(new GridBagLayout());
1899: /*
1900: * Sets up the scrollbars.
1901: */
1902: if ((type & TRANSLATE_X) != 0) {
1903: scrollbarX = new JScrollBar(JScrollBar.HORIZONTAL);
1904: scrollbarX.setUnitIncrement((int) (AMOUNT_TRANSLATE));
1905: scrollbarX
1906: .setBlockIncrement((int) (AMOUNT_TRANSLATE * ENHANCEMENT_FACTOR));
1907: } else {
1908: scrollbarX = null;
1909: }
1910: if ((type & TRANSLATE_Y) != 0) {
1911: scrollbarY = new JScrollBar(JScrollBar.VERTICAL);
1912: scrollbarY.setUnitIncrement((int) (AMOUNT_TRANSLATE));
1913: scrollbarY
1914: .setBlockIncrement((int) (AMOUNT_TRANSLATE * ENHANCEMENT_FACTOR));
1915: } else {
1916: scrollbarY = null;
1917: }
1918: /*
1919: * Adds the scrollbars in the scroll pane.
1920: */
1921: final GridBagConstraints c = new GridBagConstraints();
1922: if (scrollbarX != null) {
1923: c.gridx = 0;
1924: c.weightx = 1;
1925: c.gridy = 1;
1926: c.weighty = 0;
1927: c.fill = c.HORIZONTAL;
1928: add(scrollbarX, c);
1929: }
1930: if (scrollbarY != null) {
1931: c.gridx = 1;
1932: c.weightx = 0;
1933: c.gridy = 0;
1934: c.weighty = 1;
1935: c.fill = c.VERTICAL;
1936: add(scrollbarY, c);
1937: }
1938: if (scrollbarX != null && scrollbarY != null) {
1939: final JComponent corner = new JPanel();
1940: corner.setOpaque(true);
1941: c.gridx = 1;
1942: c.weightx = 0;
1943: c.gridy = 1;
1944: c.weighty = 0;
1945: c.fill = c.BOTH;
1946: add(corner, c);
1947: }
1948: c.fill = c.BOTH;
1949: c.gridx = 0;
1950: c.weightx = 1;
1951: c.gridy = 0;
1952: c.weighty = 1;
1953: add(ZoomPane.this , c);
1954: }
1955:
1956: /**
1957: * Convenience method which fetches a scrollbar model. Should be a static method,
1958: * but compiler doesn't allow this.
1959: */
1960: private/*static*/BoundedRangeModel getModel(
1961: final JScrollBar bar) {
1962: return (bar != null) ? bar.getModel() : null;
1963: }
1964:
1965: /**
1966: * Invoked when this {@code ScrollPane} is added in a {@link Container}.
1967: * This method registers all required listeners.
1968: */
1969: public void addNotify() {
1970: super .addNotify();
1971: tieModels(getModel(scrollbarX), getModel(scrollbarY));
1972: ZoomPane.this
1973: .addPropertyChangeListener("zoom.insets", this );
1974: }
1975:
1976: /**
1977: * Invoked when this {@code ScrollPane} is removed from a {@link Container}.
1978: * This method unregisters all listeners.
1979: */
1980: public void removeNotify() {
1981: ZoomPane.this .removePropertyChangeListener("zoom.insets",
1982: this );
1983: untieModels(getModel(scrollbarX), getModel(scrollbarY));
1984: super .removeNotify();
1985: }
1986:
1987: /**
1988: * Invoked when the zoomable area changes. This method will adjust scrollbar's
1989: * insets in order to keep scrollbars aligned in front of the zoomable area.
1990: *
1991: * Note: in the current version, this is an undocumented capability. Class {@link RangeBar}
1992: * uses it, but it is experimental. It may change in a future version.
1993: */
1994: public void propertyChange(final PropertyChangeEvent event) {
1995: final Insets old = (Insets) event.getOldValue();
1996: final Insets insets = (Insets) event.getNewValue();
1997: final GridBagLayout layout = (GridBagLayout) getLayout();
1998: if (scrollbarX != null
1999: && (old.left != insets.left || old.right != insets.right)) {
2000: final GridBagConstraints c = layout
2001: .getConstraints(scrollbarX);
2002: c.insets.left = insets.left;
2003: c.insets.right = insets.right;
2004: layout.setConstraints(scrollbarX, c);
2005: scrollbarX.invalidate();
2006: }
2007: if (scrollbarY != null
2008: && (old.top != insets.top || old.bottom != insets.bottom)) {
2009: final GridBagConstraints c = layout
2010: .getConstraints(scrollbarY);
2011: c.insets.top = insets.top;
2012: c.insets.bottom = insets.bottom;
2013: layout.setConstraints(scrollbarY, c);
2014: scrollbarY.invalidate();
2015: }
2016: }
2017: }
2018:
2019: /**
2020: * Synchronises the position and the range of the models <var>x</var> and <var>y</var> with the
2021: * position of the zoom. The models <var>x</var> and <var>y</var> are generally associated with
2022: * horizontal and vertical scrollbars. When the position of a scrollbar is adjusted, the zoom
2023: * is consequently adjusted. Inversely, when the zoom is modified, the positions and ranges of
2024: * the scrollbars are consequently adjusted.
2025: *
2026: * @param x Model of the horizontal scrollbar or {@code null} if there isn't one.
2027: * @param y Model of the vertical scrollbar or {@code null} if there isn't one.
2028: */
2029: public void tieModels(final BoundedRangeModel x,
2030: final BoundedRangeModel y) {
2031: if (x != null || y != null) {
2032: final Synchronizer listener = new Synchronizer(x, y);
2033: addZoomChangeListener(listener);
2034: if (x != null)
2035: x.addChangeListener(listener);
2036: if (y != null)
2037: y.addChangeListener(listener);
2038: }
2039: }
2040:
2041: /**
2042: * Cancels the synchronisation between the specified <var>x</var> and <var>y</var> models
2043: * and the zoom of this {@code ZoomPane} object. The {@link ChangeListener} and
2044: * {@link ZoomChangeListener} objects that were created are deleted.
2045: *
2046: * @param x Model of the horizontal scrollbar or {@code null} if there isn't one.
2047: * @param y Model of the vertical scrollbar or {@code null} if there isn't one.
2048: */
2049: public void untieModels(final BoundedRangeModel x,
2050: final BoundedRangeModel y) {
2051: final EventListener[] listeners = getListeners(ZoomChangeListener.class);
2052: for (int i = 0; i < listeners.length; i++) {
2053: if (listeners[i] instanceof Synchronizer) {
2054: final Synchronizer s = (Synchronizer) listeners[i];
2055: if (s.xm == x && s.ym == y) {
2056: removeZoomChangeListener(s);
2057: if (x != null)
2058: x.removeChangeListener(s);
2059: if (y != null)
2060: y.removeChangeListener(s);
2061: }
2062: }
2063: }
2064: }
2065:
2066: /**
2067: * Object responsible for synchronizing a {@link JScrollPane} object with scrollbars.
2068: * Whilst not generally useful, it would be possible to synchronize several pairs of
2069: * {@link BoundedRangeModel} objects on one {@code ZoomPane} object.
2070: *
2071: * @author Martin Desruisseaux
2072: * @version $Id: ZoomPane.java 27862 2007-11-12 19:51:19Z desruisseaux $
2073: */
2074: private final class Synchronizer implements ChangeListener,
2075: ZoomChangeListener {
2076: /**
2077: * Model to synchronize with {@link ZoomPane}.
2078: */
2079: public final BoundedRangeModel xm, ym;
2080:
2081: /**
2082: * Indicates whether the scrollbars are being adjusted in response to {@link #zoomChanged}.
2083: * If this is the case, {@link #stateChanged} mustn't make any other adjustments.
2084: */
2085: private transient boolean isAdjusting;
2086:
2087: /**
2088: * Cached {@code ZoomPane} bounds. Used in order to avoid too many object allocations
2089: * on the heap.
2090: */
2091: private transient Rectangle bounds;
2092:
2093: /**
2094: * Constructs an object which synchronises a pair of {@link BoundedRangeModel} with
2095: * {@link ZoomPane}.
2096: */
2097: public Synchronizer(final BoundedRangeModel xm,
2098: final BoundedRangeModel ym) {
2099: this .xm = xm;
2100: this .ym = ym;
2101: }
2102:
2103: /**
2104: * Method called automatically each time the position of one of the scrollbars changes.
2105: */
2106: public void stateChanged(final ChangeEvent event) {
2107: if (!isAdjusting) {
2108: final boolean valueIsAdjusting = ((BoundedRangeModel) event
2109: .getSource()).getValueIsAdjusting();
2110: if (paintingWhileAdjusting || !valueIsAdjusting) {
2111: /*
2112: * Scroll view coordinates are computed using the following steps:
2113: *
2114: * 1) Get the logical coordinates for the whole area.
2115: * 2) Transform to pixel space using current zoom.
2116: * 3) Clip to the scrollbar's position (in pixels).
2117: * 4) Transform back to the logical space.
2118: * 5) Set the visible area to the resulting rectangle.
2119: */
2120: Rectangle2D area = getArea();
2121: if (isValid(area)) {
2122: area = XAffineTransform.transform(zoom, area,
2123: null);
2124: double x = area.getX();
2125: double y = area.getY();
2126: double width, height;
2127: if (xm != null) {
2128: x += xm.getValue();
2129: width = xm.getExtent();
2130: } else {
2131: width = area.getWidth();
2132: }
2133: if (ym != null) {
2134: y += ym.getValue();
2135: height = ym.getExtent();
2136: } else {
2137: height = area.getHeight();
2138: }
2139: area.setRect(x, y, width, height);
2140: bounds = getBounds(bounds);
2141: try {
2142: area = XAffineTransform.inverseTransform(
2143: zoom, area, area);
2144: try {
2145: isAdjusting = true;
2146: transform(setVisibleArea(area,
2147: bounds = getBounds(bounds), 0));
2148: } finally {
2149: isAdjusting = false;
2150: }
2151: } catch (NoninvertibleTransformException exception) {
2152: unexpectedException("stateChanged",
2153: exception);
2154: }
2155: }
2156: }
2157: if (!valueIsAdjusting) {
2158: zoomChanged(null);
2159: }
2160: }
2161: }
2162:
2163: /**
2164: * Method called each time the zoom changes.
2165: *
2166: * @param change Ignored. Can be null and will effectively sometimes be null.
2167: */
2168: public void zoomChanged(final ZoomChangeEvent change) {
2169: if (!isAdjusting) {
2170: Rectangle2D area = getArea();
2171: if (isValid(area)) {
2172: area = XAffineTransform.transform(zoom, area, null);
2173: try {
2174: isAdjusting = true;
2175: setRangeProperties(xm, area.getX(), getWidth(),
2176: area.getWidth());
2177: setRangeProperties(ym, area.getY(),
2178: getHeight(), area.getHeight());
2179: } finally {
2180: isAdjusting = false;
2181: }
2182: }
2183: }
2184: }
2185: }
2186:
2187: /**
2188: * Adjusts the values of a model. The minimums and maximums are adjusted as needed in order to
2189: * include the value and its range. This adjustment is necessary in order to avoid chaotic
2190: * behaviour when the user drags the slider whilst a part of the graphic is outside the zone
2191: * initially planned for {@link #getArea}.
2192: */
2193: private static void setRangeProperties(
2194: final BoundedRangeModel model, final double value,
2195: final int extent, final double max) {
2196: if (model != null) {
2197: final int pos = (int) Math.round(-value);
2198: model.setRangeProperties(pos, extent, Math.min(0, pos),
2199: Math.max((int) Math.round(max), pos + extent),
2200: false);
2201: }
2202: }
2203:
2204: /**
2205: * Modifies the position in pixels of the visible part of {@code ZoomPane}. {@code viewSize}
2206: * is the size {@code ZoomPane} would be (in pixels) if its visible surface covered the whole
2207: * of the {@link #getArea} region with the current zoom (Note: {@code viewSize} can be obtained
2208: * by {@link #getPreferredSize} if {@link #setPreferredSize} hasn't been called with a non-null
2209: * value). Therefore, by definition, the region {@link #getArea} converted into pixel space
2210: * would give the rectangle
2211: * <code>bounds=Rectangle(0, 0, ,viewSize.width, ,viewSize.height)</code>.
2212: * <p>
2213: * This {@code scrollRectToVisible} method allows us to define the sub-region of {@code bounds}
2214: * which must appear in the {@code ZoomPane} window.
2215: */
2216: public void scrollRectToVisible(final Rectangle rect) {
2217: Rectangle2D area = getArea();
2218: if (isValid(area)) {
2219: area = XAffineTransform.transform(zoom, area, null);
2220: area.setRect(area.getX() + rect.getX(), area.getY()
2221: + rect.getY(), rect.getWidth(), rect.getHeight());
2222: try {
2223: setVisibleArea(XAffineTransform.inverseTransform(zoom,
2224: area, area));
2225: } catch (NoninvertibleTransformException exception) {
2226: unexpectedException("scrollRectToVisible", exception);
2227: }
2228: }
2229: }
2230:
2231: /**
2232: * Indicates whether or not this {@code ZoomPane} object should be repainted when the user
2233: * moves the scrollbar slider. The scrollbars (or other models) involved are those which have
2234: * been synchronised with this {@code ZoomPane} object through the {@link #tieModels} method.
2235: * The default value is {@code false}, which means that {@code ZoomPane} will wait until the
2236: * user releases the slider before repainting.
2237: */
2238: public boolean isPaintingWhileAdjusting() {
2239: return paintingWhileAdjusting;
2240: }
2241:
2242: /**
2243: * Defines whether or not this {@code ZoomPane} object should repaint the map when the user
2244: * moves the scrollbar slider. A fast computer is recommended if this flag is to be set to
2245: * {@code true}.
2246: */
2247: public void setPaintingWhileAdjusting(final boolean flag) {
2248: paintingWhileAdjusting = flag;
2249: }
2250:
2251: /**
2252: * Declares that a part of this pane needs to be repainted. This method simply redefines the
2253: * method of the parent class in order to take into account a case where the magnifying glass
2254: * is displayed.
2255: */
2256: public void repaint(final long tm, final int x, final int y,
2257: final int width, final int height) {
2258: super .repaint(tm, x, y, width, height);
2259: if (magnifier != null
2260: && magnifier.intersects(x, y, width, height)) {
2261: // If the part to paint is inside the magnifying glass,
2262: // the fact that the magnifying glass is zooming in means
2263: // we have to repaint a little more than that which was requested.
2264: repaintMagnifier();
2265: }
2266: }
2267:
2268: /**
2269: * Declares that the magnifying glass needs to be repainted. A {@link #repaint()} command is
2270: * sent with the bounds of the magnifying glass as coordinates (taking into account its outline).
2271: */
2272: private void repaintMagnifier() {
2273: final Rectangle bounds = magnifier.getBounds();
2274: bounds.x -= 4;
2275: bounds.y -= 4;
2276: bounds.width += 8;
2277: bounds.height += 8;
2278: super .repaint(0, bounds.x, bounds.y, bounds.width,
2279: bounds.height);
2280: }
2281:
2282: /**
2283: * Paints the magnifying glass. This method is invoked after
2284: * {@link #paintComponent(Graphics2D)} if a magnifying glass is visible.
2285: */
2286: protected void paintMagnifier(final Graphics2D graphics) {
2287: final double centerX = magnifier.getCenterX();
2288: final double centerY = magnifier.getCenterY();
2289: final Stroke stroke = graphics.getStroke();
2290: final Paint paint = graphics.getPaint();
2291: graphics.setStroke(new BasicStroke(6));
2292: graphics.setPaint(magnifierBorder);
2293: graphics.draw(magnifier);
2294: graphics.setStroke(stroke);
2295: graphics.clip(magnifier); // Coordinates in pixels!
2296: graphics.setPaint(magnifierGlass);
2297: graphics.fill(magnifier.getBounds2D());
2298: graphics.setPaint(paint);
2299: graphics.translate(+centerX, +centerY);
2300: graphics.scale(magnifierPower, magnifierPower);
2301: graphics.translate(-centerX, -centerY);
2302: // Note: the transformations performed here must be identical to those
2303: // performed in {@link #pixelToLogical}.
2304: paintComponent(graphics);
2305: }
2306:
2307: /**
2308: * Paints this component. Subclass must override this method in order to draw the
2309: * {@code ZoomPane} content. For most implementations, the first line in this method will be
2310: * <code>graphics.transform({@link #zoom})</code>.
2311: */
2312: protected abstract void paintComponent(final Graphics2D graphics);
2313:
2314: /**
2315: * Prints this component. The default implementation invokes
2316: * {@link #paintComponent(Graphics2D)}.
2317: */
2318: protected void printComponent(final Graphics2D graphics) {
2319: paintComponent(graphics);
2320: }
2321:
2322: /**
2323: * Paints this component. This method is declared final in order to avoid unintentional
2324: * overriding. Override {@link #paintComponent(Graphics2D)} instead.
2325: */
2326: protected final void paintComponent(final Graphics graphics) {
2327: flag = IS_PAINTING;
2328: super .paintComponent(graphics);
2329: /*
2330: * The JComponent.paintComponent(...) method creates a temporary Graphics2D object, then
2331: * calls ComponentUI.update(...) with this graphic as a parameter. This method clears the
2332: * screen background then calls ComponentUI.paint(...). This last method has been redefined
2333: * further up (our {@link #UI}) object in such a way that it calls itself
2334: * paintComponent(Graphics2D). A complicated path, but we don't have much
2335: * choice and it is, after all, quite efficient.
2336: */
2337: if (magnifier != null) {
2338: flag = IS_PAINTING_MAGNIFIER;
2339: super .paintComponent(graphics);
2340: }
2341: }
2342:
2343: /**
2344: * Prints this component. This method is declared final in order to avoid unintentional
2345: * overriding. Override {@link #printComponent(Graphics2D)} instead.
2346: */
2347: protected final void printComponent(final Graphics graphics) {
2348: flag = IS_PRINTING;
2349: super .paintComponent(graphics);
2350: /*
2351: * Ne pas appeller 'super.printComponent' parce qu'on ne
2352: * veut pas qu'il appelle notre 'paintComponent' ci-haut.
2353: */
2354: }
2355:
2356: /**
2357: * Returns the size (in pixels) that {@code ZoomPane} would have if it displayed the whole of
2358: * the {@link #getArea} region with the current zoom ({@link #zoom}). This method is practical
2359: * for determining the maximum values to assign to the scrollbars. For example, the horizontal
2360: * bar could cover the range {@code [0..viewSize.width]} whilst the vertical bar could cover
2361: * the range {@code [0..viewSize.height]}.
2362: */
2363: private final Dimension getViewSize() {
2364: if (!visibleArea.isEmpty()) {
2365: Rectangle2D area = getArea();
2366: if (isValid(area)) {
2367: area = XAffineTransform.transform(zoom, area, null);
2368: return new Dimension((int) Math.rint(area.getWidth()),
2369: (int) Math.rint(area.getHeight()));
2370: }
2371: return getSize();
2372: }
2373: return new Dimension(DEFAULT_SIZE, DEFAULT_SIZE);
2374: }
2375:
2376: /**
2377: * Returns the Insets of this component. This method works like {@code super.getInsets(insets)},
2378: * but accepts a null argument. This method can be redefined if it is necessary to perform zooms
2379: * on a part of the graphic rather than the whole thing.
2380: */
2381: public Insets getInsets(final Insets insets) {
2382: return super .getInsets((insets != null) ? insets : new Insets(
2383: 0, 0, 0, 0));
2384: }
2385:
2386: /**
2387: * Returns the Insets of this component. This method is declared final in order to avoid
2388: * confusion. If you want to return other Insets you must redefine {@link #getInsets(Insets)}.
2389: */
2390: public final Insets getInsets() {
2391: return getInsets(null);
2392: }
2393:
2394: /**
2395: * Informs {@code ZoomPane} that the GUI has changed.
2396: * The user doesn't have to call this method directly.
2397: */
2398: public void updateUI() {
2399: navigationPopupMenu = null;
2400: super .updateUI();
2401: setUI(UI);
2402: }
2403:
2404: /**
2405: * Invoked when an affine transform that should be invertible is not.
2406: * Default implementation logs the stack trace and resets the zoom.
2407: *
2408: * @param methodName The caller's method name.
2409: * @param exception The exception.
2410: */
2411: private void unexpectedException(final String methodName,
2412: final NoninvertibleTransformException exception) {
2413: zoom.setToIdentity();
2414: Logging.unexpectedException("org.geotools.gui", ZoomPane.class,
2415: methodName, exception);
2416: }
2417:
2418: /**
2419: * Invoked when an unexpected exception occurs.
2420: * Default implementation logs the stack trace.
2421: *
2422: * @param methodName The caller's method name.
2423: * @param exception The exception.
2424: */
2425: private static void unexpectedException(final String methodName,
2426: final RuntimeException exception) {
2427: Logging.unexpectedException("org.geotools.gui", ZoomPane.class,
2428: methodName, exception);
2429: }
2430:
2431: /**
2432: * Convenience method logging an area setting from the {@code ZoomPane} class. This
2433: * method is invoked from {@link #setPreferredArea} and {@link #setVisibleArea}.
2434: *
2435: * @param methodName The caller's method name (e.g. <code>"setArea"</code>).
2436: * @param area The coordinates to log (may be {@code null}).
2437: */
2438: private static void log(final String methodName,
2439: final Rectangle2D area) {
2440: log(ZoomPane.class.getName(), methodName, area);
2441: }
2442:
2443: /**
2444: * Convenience method for logging events related to area setting. Events are logged in the
2445: * {@code "org.geotools.gui"} logger with {@link Level#FINER}. {@code ZoomPane} invokes this
2446: * method for logging any [@link #setPreferredArea} and {@link #setVisibleArea} invocations.
2447: * Subclasses may invoke this method for logging some other kinds of area changes.
2448: *
2449: * @param className The fully qualified caller's class name
2450: * (e.g. {@code "org.geotools.swing.ZoomPane"}).
2451: * @param methodName The caller's method name (e.g. {@code "setArea"}).
2452: * @param area The coordinates to log (may be {@code null}).
2453: */
2454: static void log(final String className, final String methodName,
2455: final Rectangle2D area) {
2456: if (LOGGER.isLoggable(Level.FINER)) {
2457: final Double[] areaBounds;
2458: if (area != null) {
2459: areaBounds = new Double[] { new Double(area.getMinX()),
2460: new Double(area.getMaxX()),
2461: new Double(area.getMinY()),
2462: new Double(area.getMaxY()) };
2463: } else {
2464: areaBounds = new Double[4];
2465: Arrays.fill(areaBounds, new Double(Double.NaN));
2466: }
2467: final Vocabulary resources = Vocabulary.getResources(null);
2468: final LogRecord record = resources.getLogRecord(
2469: Level.FINER, VocabularyKeys.RECTANGLE_$4,
2470: areaBounds);
2471: record.setSourceClassName(className);
2472: record.setSourceMethodName(methodName);
2473: LOGGER.log(record);
2474: }
2475: }
2476:
2477: /**
2478: * Checks whether the rectangle {@code rect} is valid. The rectangle
2479: * is considered invalid if its length or width is less than or equal to 0,
2480: * or if one of its coordinates is infinite or NaN.
2481: */
2482: private static boolean isValid(final Rectangle2D rect) {
2483: if (rect == null) {
2484: return false;
2485: }
2486: final double x = rect.getX();
2487: final double y = rect.getY();
2488: final double w = rect.getWidth();
2489: final double h = rect.getHeight();
2490: return (x > Double.NEGATIVE_INFINITY
2491: && x < Double.POSITIVE_INFINITY
2492: && y > Double.NEGATIVE_INFINITY
2493: && y < Double.POSITIVE_INFINITY && w > 0
2494: && w < Double.POSITIVE_INFINITY && h > 0 && h < Double.POSITIVE_INFINITY);
2495: }
2496: }
|