Coverage Report - org.geotoolkit.gui.swing.ZoomPane
 
Classes in this File Line Coverage Branch Coverage Complexity
ZoomPane
29 %
158/543
18 %
50/276
3,112
ZoomPane$1
62 %
10/16
33 %
2/6
3,112
ZoomPane$2
7 %
1/14
0 %
0/4
3,112
ZoomPane$3
6 %
1/16
0 %
0/14
3,112
ZoomPane$4
0 %
0/3
N/A
3,112
ZoomPane$5
0 %
0/3
N/A
3,112
ZoomPane$6
0 %
0/3
N/A
3,112
ZoomPane$Listeners
25 %
2/8
N/A
3,112
ZoomPane$PointPopupMenu
0 %
0/3
N/A
3,112
ZoomPane$ScrollPane
57 %
34/59
37 %
9/24
3,112
ZoomPane$Synchronizer
0 %
0/43
0 %
0/18
3,112
 
 1  
 /*
 2  
  *    Geotoolkit.org - An Open Source Java GIS Toolkit
 3  
  *    http://www.geotoolkit.org
 4  
  *
 5  
  *    (C) 1999-2012, Open Source Geospatial Foundation (OSGeo)
 6  
  *    (C) 2009-2012, Geomatys
 7  
  *
 8  
  *    This library is free software; you can redistribute it and/or
 9  
  *    modify it under the terms of the GNU Lesser General Public
 10  
  *    License as published by the Free Software Foundation;
 11  
  *    version 2.1 of the License.
 12  
  *
 13  
  *    This library is distributed in the hope that it will be useful,
 14  
  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 15  
  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 16  
  *    Lesser General Public License for more details.
 17  
  */
 18  
 package org.geotoolkit.gui.swing;
 19  
 
 20  
 import java.util.Arrays;
 21  
 import java.util.EventListener;
 22  
 import java.util.logging.Level;
 23  
 import java.util.logging.Logger;
 24  
 import java.util.logging.LogRecord;
 25  
 import java.io.Serializable;
 26  
 
 27  
 import java.awt.*;
 28  
 import java.awt.geom.*;
 29  
 import java.awt.event.*;
 30  
 import java.awt.Window;
 31  
 import javax.swing.*;
 32  
 import javax.swing.plaf.ComponentUI;
 33  
 import javax.swing.event.ChangeEvent;
 34  
 import javax.swing.event.ChangeListener;
 35  
 import java.beans.PropertyChangeEvent;
 36  
 import java.beans.PropertyChangeListener;
 37  
 
 38  
 import org.geotoolkit.resources.Errors;
 39  
 import org.geotoolkit.resources.Vocabulary;
 40  
 import org.geotoolkit.util.logging.Logging;
 41  
 import org.geotoolkit.util.NullArgumentException;
 42  
 import org.geotoolkit.gui.swing.event.ZoomChangeEvent;
 43  
 import org.geotoolkit.gui.swing.event.ZoomChangeListener;
 44  
 import org.geotoolkit.display.shape.DoubleDimension2D;
 45  
 import org.geotoolkit.referencing.operation.matrix.XAffineTransform;
 46  
 
 47  
 import static java.awt.GridBagConstraints.*;
 48  
 
 49  
 
 50  
 /**
 51  
  * Base class for widget with a zoomable content. User can perform zooms using keyboard, menu
 52  
  * or mouse. Subclasses must provide the content to be paint with the following methods, which
 53  
  * need to be overridden:
 54  
  *
 55  
  * <ul>
 56  
  *   <li><p>{@link #getArea()}, which must return a bounding box for the content to paint. This
 57  
  *   area can be expressed in arbitrary units. For example, an object wanting to display a
 58  
  *   geographic map with a content ranging from 10° to 15°E and 40° to 45°N should override
 59  
  *   this method as follows:
 60  
  *
 61  
  * {@preformat java
 62  
  *     public Rectangle2D getArea() {
 63  
  *         return new Rectangle2D.Double(10, 40, 15-10, 45-40);
 64  
  *     }
 65  
  * }</p></li>
 66  
  *
 67  
  *   <li><p>{@link #paintComponent(Graphics2D)}, which must paint the widget content. Implementations
 68  
  *   must invoke <code>graphics.transform({@link #zoom})</code> somewhere in their code in order to
 69  
  *   perform the zoom. Note that, by default, the {@linkplain #zoom} is initialized in such a way
 70  
  *   that the <var>y</var> axis points upwards, like the convention in geometry. This is opposed to
 71  
  *   the default Java2D axis orientation, where the <var>y</var> axis points downwards. The Java2D
 72  
  *   convention is appropriate for text rendering - consequently implementations wanting to paint
 73  
  *   text should use the default transform (the one provided by {@link Graphics2D}) for that
 74  
  *   purpose. Example:
 75  
  *
 76  
  * {@preformat java
 77  
  *     protected void paintComponent(final Graphics2D graphics) {
 78  
  *         graphics.clip(getZoomableBounds(null));
 79  
  *         final AffineTransform textTr = graphics.getTransform();
 80  
  *         graphics.transform(zoom);
 81  
  *         // Paint the widget here, using logical coordinates.
 82  
  *         // The coordinate system is the same as getArea()'s one.
 83  
  *         graphics.setTransform(textTr);
 84  
  *         // Paint any text here, in pixel coordinates.
 85  
  *     }
 86  
  * }</p></li>
 87  
  *
 88  
  *   <li><p>{@link #reset}, which sets up the initial {@linkplain #zoom}. Overriding this method
 89  
  *   is optional since the default implementation is appropriate in many cases. This default
 90  
  *   implementation setups the initial zoom in such a way that the following relation
 91  
  *   approximately hold: <cite>Logical coordinates provided by {@link #getPreferredArea()},
 92  
  *   after an affine transform described by {@link #zoom}, match pixel coordinates provided
 93  
  *   by {@link #getZoomableBounds(Rectangle)}.</cite></p></li>
 94  
  * </ul>
 95  
  *
 96  
  * The "preferred area" is initially the same as {@link #getArea()}. The user can specify a
 97  
  * different preferred area with {@link #setPreferredArea(Rectangle2D)}. The user can also
 98  
  * reduce zoomable bounds by inserting an empty border around the widget, e.g.:
 99  
  *
 100  
  * {@preformat java
 101  
  *     setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));
 102  
  * }
 103  
  *
 104  
  * {@section Zoom actions}
 105  
  * Whatever action is performed by the user, all zoom commands are translated as calls to
 106  
  * {@link #transform(AffineTransform)}. Derived classes can redefine this method if they want
 107  
  * to take particular actions during zooms, for example, modifying the minimum and maximum of
 108  
  * a graph's axes. The table below shows the keyboard presses assigned to each zoom:
 109  
  * <p>
 110  
  * <TABLE ALIGN="CENTER" CELLPADDING="16"><TR>
 111  
  * <TD><TABLE ALIGN="CENTER" BORDER="2">
 112  
  * <TR BGCOLOR="#CCCCFF"><TH>Key</TH>                 <TH>Purpose</TH>     <TH>{@link Action} name</TH></TR>
 113  
  * <TR><TD><IMG SRC="doc-files/key-up.png"></TD>      <TD>Scroll up</TD>   <TD><code>"Up"</code></TD></TR>
 114  
  * <TR><TD><IMG SRC="doc-files/key-down.png"></TD>    <TD>Scroll down</TD> <TD><code>"Down"</code></TD></TR>
 115  
  * <TR><TD><IMG SRC="doc-files/key-left.png"></TD>    <TD>Scroll left</TD> <TD><code>"Left"</code></TD></TR>
 116  
  * <TR><TD><IMG SRC="doc-files/key-right.png"></TD>   <TD>Scroll right</TD><TD><code>"Right"</code></TD></TR>
 117  
  * </TABLE></TD><TD><TABLE ALIGN="CENTER" BORDER="2">
 118  
  * <TR BGCOLOR="#CCCCFF"><TH>Key</TH>                 <TH>Purpose</TH>     <TH>{@link Action} name</TH></TR>
 119  
  * <TR><TD><IMG SRC="doc-files/key-pageDown.png"></TD><TD>Zoom in</TD>     <TD><code>"ZoomIn"</code></TD></TR>
 120  
  * <TR><TD><IMG SRC="doc-files/key-pageUp.png"></TD>  <TD>Zoom out</TD>    <TD><code>"ZoomOut"</code></TD></TR>
 121  
  * <TR><TD><IMG SRC="doc-files/key-end.png"></TD>     <TD>Maximal zoom</TD><TD><code>"Zoom"</code></TD></TR>
 122  
  * <TR><TD><IMG SRC="doc-files/key-home.png"></TD>    <TD>Default zoom</TD><TD><code>"Reset"</code></TD></TR>
 123  
  * </TABLE></TD><TD><TABLE ALIGN="CENTER" BORDER="2">
 124  
  * <TR BGCOLOR="#CCCCFF"><TH>Key</TH><TH>Purpose</TH><TH>{@link Action} name</TH></TR>
 125  
  * <TR><TD>Ctrl+<IMG SRC="doc-files/key-left.png"></TD><TD>Anti-clockwise rotation</TD><TD><code>"RotateLeft"</code></TD></TR>
 126  
  * <TR><TD>Ctrl+<IMG SRC="doc-files/key-right.png"></TD><TD>Clockwise rotation</TD><TD><code>"RotateRight"</code></TD></TR>
 127  
  * </TABLE></TD></TR></TABLE>
 128  
  * <p>
 129  
  * In this table, the last column gives the Strings that identify the different actions
 130  
  * which manage the zooms. For example, to zoom in, we must write
 131  
  * <code>{@linkplain #getActionMap() getActionMap()}.get("ZoomIn")</code>.
 132  
  *
 133  
  * {@section Scroll pane}
 134  
  * <strong>{@link JScrollPane} objects are not suitable for adding scrollbars to a
 135  
  * {@code ZoomPane} object.</strong> Instead, use {@link #createScrollPane}. Once again, all
 136  
  * movements performed by the user through the scrollbars will be translated by calls to
 137  
  * {@link #transform(AffineTransform)}.
 138  
  *
 139  
  * <table cellspacing="24" cellpadding="12" align="center"><tr valign="top"><td>
 140  
  * <img src="doc-files/ZoomPane.png">
 141  
  * </td><td width="500" bgcolor="lightblue">
 142  
  * {@section Demo}
 143  
  * The image on the left side gives an example with a simple implementation drawing a
 144  
  * few geometric shapes. The menu and the optional magnifier glass are produced by this
 145  
  * {@code ZoomPane} class.
 146  
  * <p>
 147  
  * To try this component in your browser, see the
 148  
  * <a href="http://www.geotoolkit.org/demos/geotk-simples/applet/ZoomPane.html">demonstration applet</a>.
 149  
  * </td></tr></table>
 150  
  *
 151  
  * @author Martin Desruisseaux (MPO, IRD)
 152  
  * @version 3.00
 153  
  *
 154  
  * @since 1.1
 155  
  * @module
 156  
  */
 157  
 @SuppressWarnings("serial")
 158  12
 public abstract class ZoomPane extends JComponent implements DeformableViewer {
 159  
     /**
 160  
      * The logger for zoom events.
 161  
      */
 162  1
     private static final Logger LOGGER = Logging.getLogger(ZoomPane.class);
 163  
 
 164  
     /**
 165  
      * Small number for floating point comparisons.
 166  
      */
 167  
     private static final double EPS = 1E-6;
 168  
 
 169  
     /**
 170  
      * Minimum width and height of this component.
 171  
      */
 172  
     private static final int MINIMUM_SIZE = 10;
 173  
 
 174  
     /**
 175  
      * Default width and height of this component.
 176  
      */
 177  
     private static final int DEFAULT_SIZE = 400;
 178  
 
 179  
     /**
 180  
      * Default width and height of the magnifying glass.
 181  
      */
 182  
     private static final int DEFAULT_MAGNIFIER_SIZE = 150;
 183  
 
 184  
     /**
 185  
      * Default color with which to tint magnifying glass.
 186  
      */
 187  1
     private static final Paint DEFAULT_MAGNIFIER_GLASS = new Color(209, 225, 243);
 188  
 
 189  
     /**
 190  
      * Default color of the magnifying glass's border.
 191  
      */
 192  1
     private static final Paint DEFAULT_MAGNIFIER_BORDER = new Color(110, 129, 177);
 193  
 
 194  
     /**
 195  
      * Constant indicating the scale changes on the <var>x</var> axis.
 196  
      */
 197  
     public static final int SCALE_X = (1 << 0);
 198  
 
 199  
     /**
 200  
      * Constant indicating the scale changes on the <var>y</var> axis.
 201  
      */
 202  
     public static final int SCALE_Y = (1 << 1);
 203  
 
 204  
     /**
 205  
      * Constant indicating the scale changes on the <var>x</var> and <var>y</var> axes, with the
 206  
      * added condition that these changes must be uniform.  This flag combines {@link #SCALE_X}
 207  
      * and {@link #SCALE_Y}. The inverse, however, (<code>{@link #SCALE_X}|{@link #SCALE_Y}</code>)
 208  
      * doesn't imply {@code UNIFORM_SCALE}.
 209  
      */
 210  
     public static final int UNIFORM_SCALE = SCALE_X | SCALE_Y | (1 << 2);
 211  
 
 212  
     /**
 213  
      * Constant indicating the translations on the <var>x</var> axis.
 214  
      */
 215  
     public static final int TRANSLATE_X = (1 << 3);
 216  
 
 217  
     /**
 218  
      * Constant indicating the translations on the <var>y</var> axis.
 219  
      */
 220  
     public static final int TRANSLATE_Y = (1 << 4);
 221  
 
 222  
     /**
 223  
      * Constant indicating a rotation.
 224  
      */
 225  
     public static final int ROTATE  = (1 << 5);
 226  
 
 227  
     /**
 228  
      * Constant indicating the resetting of scale, rotation and translation to a default value
 229  
      * which makes the whole graphic appear in a window. This command is translated by a call
 230  
      * to {@link #reset}.
 231  
      */
 232  
     public static final int RESET = (1 << 6);
 233  
 
 234  
     /**
 235  
      * Constant indicating default zoom close to the maximum permitted zoom. This zoom should
 236  
      * allow details of the graphic to be seen without being overly big.
 237  
      * <p>
 238  
      * Note: this flag will only have any effect if at least one of the
 239  
      * {@link #SCALE_X} and {@link #SCALE_Y} flags is not also specified.
 240  
      */
 241  
     public static final int DEFAULT_ZOOM = (1 << 7);
 242  
 
 243  
     /**
 244  
      * Mask representing the combination of all flags.
 245  
      */
 246  
     private static final int MASK = SCALE_X | SCALE_Y | UNIFORM_SCALE | TRANSLATE_X | TRANSLATE_Y |
 247  
                                     ROTATE | RESET | DEFAULT_ZOOM;
 248  
 
 249  
     /**
 250  
      * Number of pixels by which to move the content of {@code ZoomPane} during translations.
 251  
      */
 252  
     private static final double AMOUNT_TRANSLATE = 10;
 253  
 
 254  
     /**
 255  
      * Zoom factor.  This factor must be greater than 1.
 256  
      */
 257  
     private static final double AMOUNT_SCALE = 1.03125;
 258  
 
 259  
     /**
 260  
      * Rotation angle.
 261  
      */
 262  
     private static final double AMOUNT_ROTATE = Math.PI / 90;
 263  
 
 264  
     /**
 265  
      * Factor by which to multiply the {@link #ACTION_AMOUNT} numbers
 266  
      * when the "Shift" key is kept pressed.
 267  
      */
 268  
     private static final double ENHANCEMENT_FACTOR = 7.5;
 269  
 
 270  
     /**
 271  
      * Flag indicating that a paint is in progress.
 272  
      */
 273  
     private static final int IS_PAINTING = 0;
 274  
 
 275  
     /**
 276  
      * Flag indicating that a paint of the magnifying glass is in progress.
 277  
      */
 278  
     private static final int IS_PAINTING_MAGNIFIER = 1;
 279  
 
 280  
     /**
 281  
      * Flag indicating that a print is in progress.
 282  
      */
 283  
     private static final int IS_PRINTING = 2;
 284  
 
 285  
     /**
 286  
      * List of keys which will identify the zoom actions. These keys also identify the resources
 287  
      * to use in order to make the description appear in the user's language.
 288  
      */
 289  1
     private static final String[] ACTION_ID = {
 290  
         /*[0] Left        */ "Left",
 291  
         /*[1] Right       */ "Right",
 292  
         /*[2] Up          */ "Up",
 293  
         /*[3] Down        */ "Down",
 294  
         /*[4] ZoomIn      */ "ZoomIn",
 295  
         /*[5] ZoomOut     */ "ZoomOut",
 296  
         /*[6] ZoomMax     */ "ZoomMax",
 297  
         /*[7] Reset       */ "Reset",
 298  
         /*[8] RotateLeft  */ "RotateLeft",
 299  
         /*[9] RotateRight */ "RotateRight"
 300  
     };
 301  
 
 302  
     /**
 303  
      * List of resource keys, to construct the menus in the user's language.
 304  
      */
 305  1
     private static final int[] RESOURCE_ID = {
 306  
         /*[0] Left        */ Vocabulary.Keys.LEFT,
 307  
         /*[1] Right       */ Vocabulary.Keys.RIGHT,
 308  
         /*[2] Up          */ Vocabulary.Keys.UP,
 309  
         /*[3] Down        */ Vocabulary.Keys.DOWN,
 310  
         /*[4] ZoomIn      */ Vocabulary.Keys.ZOOM_IN,
 311  
         /*[5] ZoomOut     */ Vocabulary.Keys.ZOOM_OUT,
 312  
         /*[6] ZoomMax     */ Vocabulary.Keys.ZOOM_MAX,
 313  
         /*[7] Reset       */ Vocabulary.Keys.RESET,
 314  
         /*[8] RotateLeft  */ Vocabulary.Keys.ROTATE_LEFT,
 315  
         /*[9] RotateRight */ Vocabulary.Keys.ROTATE_RIGHT
 316  
     };
 317  
 
 318  
     /**
 319  
      * List of default keystrokes used to perform zooms. The elements of this table go in pairs.
 320  
      * The even indexes indicate the keystroke whilst the odd indexes indicate the modifier
 321  
      * (CTRL or SHIFT for example). To obtain the {@link KeyStroke} object for a numbered action
 322  
      * <var>i</var>, we can use the following code:
 323  
      *
 324  
      * {@preformat java
 325  
      *     final int key = DEFAULT_KEYBOARD[(i << 1)+0];
 326  
      *     final int mdf = DEFAULT_KEYBOARD[(i << 1)+1];
 327  
      *     KeyStroke stroke = KeyStroke.getKeyStroke(key, mdf);
 328  
      * }
 329  
      */
 330  1
     private static final int[] ACTION_KEY = {
 331  
         /*[0] Left        */ KeyEvent.VK_LEFT,      0,
 332  
         /*[1] Right       */ KeyEvent.VK_RIGHT,     0,
 333  
         /*[2] Up          */ KeyEvent.VK_UP,        0,
 334  
         /*[3] Down        */ KeyEvent.VK_DOWN,      0,
 335  
         /*[4] ZoomIn      */ KeyEvent.VK_PAGE_UP,   0,
 336  
         /*[5] ZoomOut     */ KeyEvent.VK_PAGE_DOWN, 0,
 337  
         /*[6] ZoomMax     */ KeyEvent.VK_END,       0,
 338  
         /*[7] Reset       */ KeyEvent.VK_HOME,      0,
 339  
         /*[8] RotateLeft  */ KeyEvent.VK_LEFT,      KeyEvent.CTRL_MASK,
 340  
         /*[9] RotateRight */ KeyEvent.VK_RIGHT,     KeyEvent.CTRL_MASK
 341  
     };
 342  
 
 343  
     /**
 344  
      * Connstants indicating the type of action to perform: translation, zoom or rotation.
 345  
      */
 346  1
     private static final short[] ACTION_TYPE = {
 347  
         /*[0] Left        */ (short) TRANSLATE_X,
 348  
         /*[1] Right       */ (short) TRANSLATE_X,
 349  
         /*[2] Up          */ (short) TRANSLATE_Y,
 350  
         /*[3] Down        */ (short) TRANSLATE_Y,
 351  
         /*[4] ZoomIn      */ (short) SCALE_X | SCALE_Y,
 352  
         /*[5] ZoomOut     */ (short) SCALE_X | SCALE_Y,
 353  
         /*[6] ZoomMax     */ (short) DEFAULT_ZOOM,
 354  
         /*[7] Reset       */ (short) RESET,
 355  
         /*[8] RotateLeft  */ (short) ROTATE,
 356  
         /*[9] RotateRight */ (short) ROTATE
 357  
     };
 358  
 
 359  
     /**
 360  
      * Amounts by which to translate, zoom or rotate the contents of the window.
 361  
      */
 362  1
     private static final double[] ACTION_AMOUNT = {
 363  
         /*[0] Left        */  +AMOUNT_TRANSLATE,
 364  
         /*[1] Right       */  -AMOUNT_TRANSLATE,
 365  
         /*[2] Up          */  +AMOUNT_TRANSLATE,
 366  
         /*[3] Down        */  -AMOUNT_TRANSLATE,
 367  
         /*[4] ZoomIn      */   AMOUNT_SCALE,
 368  
         /*[5] ZoomOut     */ 1/AMOUNT_SCALE,
 369  
         /*[6] ZoomMax     */   Double.NaN,
 370  
         /*[7] Reset       */   Double.NaN,
 371  
         /*[8] RotateLeft  */  -AMOUNT_ROTATE,
 372  
         /*[9] RotateRight */  +AMOUNT_ROTATE
 373  
     };
 374  
 
 375  
     /**
 376  
      * List of operation types forming a group.  During creation of the
 377  
      * menus, the different groups will be separated by a menu separator.
 378  
      */
 379  1
     private static final int[] GROUP = {
 380  
         TRANSLATE_X | TRANSLATE_Y,
 381  
         SCALE_X | SCALE_Y | DEFAULT_ZOOM | RESET,
 382  
         ROTATE
 383  
     };
 384  
 
 385  
     /**
 386  
      * {@code ComponentUI} object in charge of obtaining the preferred
 387  
      * size of a {@code ZoomPane} object as well as drawing it.
 388  
      *
 389  
      * @author Martin Desruisseaux (IRD)
 390  
      * @version 3.00
 391  
      *
 392  
      * @since 2.0
 393  
      * @module
 394  
      */
 395  1
     private static final ComponentUI UI = new ComponentUI() {
 396  
         /**
 397  
          * Returns a default minimum size.
 398  
          */
 399  
         @Override
 400  
         public Dimension getMinimumSize(final JComponent c) {
 401  0
             return new Dimension(MINIMUM_SIZE, MINIMUM_SIZE);
 402  
         }
 403  
 
 404  
         /**
 405  
          * Returns the maximum size. We use the preferred size as a default maximum size.
 406  
          */
 407  
         @Override
 408  
         public Dimension getMaximumSize(final JComponent c) {
 409  0
             return getPreferredSize(c);
 410  
         }
 411  
 
 412  
         /**
 413  
          * Returns the default preferred size. User can override this
 414  
          * preferred size by invoking {@link JComponent#setPreferredSize}.
 415  
          */
 416  
         @Override
 417  
         public Dimension getPreferredSize(final JComponent c) {
 418  7
             return ((ZoomPane) c).getDefaultSize();
 419  
         }
 420  
 
 421  
         /**
 422  
          * Overrides {@link ComponentUI#update} in order to handle painting of
 423  
          * magnifying glass, which is a special case. Since the magnifying
 424  
          * glass is painted just after the normal component, we don't want to
 425  
          * clear the background before painting it.
 426  
          */
 427  
         @Override
 428  
         public void update(final Graphics g, final JComponent c) {
 429  2
             switch (((ZoomPane) c).flag) {
 430  0
                 case IS_PAINTING_MAGNIFIER: paint(g, c); break; // Avoid background clearing
 431  2
                 default:             super.update(g, c); break;
 432  
             }
 433  2
         }
 434  
 
 435  
         /**
 436  
          * Paints the component. This method basically delegates the
 437  
          * work to {@link ZoomPane#paintComponent(Graphics2D)}.
 438  
          */
 439  
         @Override
 440  
         public void paint(final Graphics g, final JComponent c) {
 441  2
             final ZoomPane pane = (ZoomPane)   c;
 442  2
             final Graphics2D gr = (Graphics2D) g;
 443  2
             switch (pane.flag) {
 444  0
                 case IS_PAINTING:           pane.paintComponent(gr); break;
 445  0
                 case IS_PAINTING_MAGNIFIER: pane.paintMagnifier(gr); break;
 446  2
                 case IS_PRINTING:           pane.printComponent(gr); break;
 447  0
                 default: throw new IllegalStateException(Integer.toString(pane.flag));
 448  
             }
 449  2
         }
 450  
     };
 451  
 
 452  
     /**
 453  
      * Object in charge of drawing a box representing the user's selection.  We
 454  
      * retain a reference to this object in order to be able to register it and
 455  
      * extract it at will from the list of objects interested in being notified
 456  
      * of the mouse movements.
 457  
      */
 458  5
     private final MouseListener mouseSelectionTracker = new MouseSelectionTracker() {
 459  
         /**
 460  
          * Returns the selection shape. This is usually a rectangle, but could
 461  
          * very well be an ellipse or any other kind of geometric shape. This
 462  
          * method asks {@link ZoomPane#getMouseSelectionShape} for the shape.
 463  
          */
 464  
         @Override
 465  
         protected Shape getModel(final MouseEvent event) {
 466  0
             final Point2D point = new Point2D.Double(event.getX(), event.getY());
 467  0
             if (getZoomableBounds().contains(point)) try {
 468  0
                 return getMouseSelectionShape(zoom.inverseTransform(point, point));
 469  0
             } catch (NoninvertibleTransformException exception) {
 470  0
                 unexpectedException("getModel", exception);
 471  
             }
 472  0
             return null;
 473  
         }
 474  
 
 475  
         /**
 476  
          * Invoked when the user finishes the selection. This method will
 477  
          * delegate the action to {@link ZoomPane#mouseSelectionPerformed}.
 478  
          * Default implementation will perform a zoom.
 479  
          */
 480  
         @Override
 481  
         protected void selectionPerformed(int ox, int oy, int px, int py) {
 482  
             try {
 483  0
                 final Shape selection = getSelectedArea(zoom);
 484  0
                 if (selection != null) {
 485  0
                     mouseSelectionPerformed(selection);
 486  
                 }
 487  0
             } catch (NoninvertibleTransformException exception) {
 488  0
                 unexpectedException("selectionPerformed", exception);
 489  0
             }
 490  0
         }
 491  
     };
 492  
 
 493  
     /**
 494  
      * Class responsible for listening out for the different events necessary for the smooth
 495  
      * working of {@link ZoomPane}. This class will listen out for mouse clicks (in order to
 496  
      * eventually claim the focus or make a contextual menu appear).  It will listen out for
 497  
      * changes in the size of the component (to adjust the zoom), etc.
 498  
      *
 499  
      * @author Martin Desruisseaux (IRD)
 500  
      * @version 3.00
 501  
      *
 502  
      * @since 2.0
 503  
      * @module
 504  
      */
 505  
     @SuppressWarnings("serial")
 506  10
     private final class Listeners extends MouseAdapter
 507  
             implements MouseWheelListener, ComponentListener, Serializable
 508  
     {
 509  0
         @Override public void mouseWheelMoved (final MouseWheelEvent event) {ZoomPane.this.mouseWheelMoved (event);}
 510  0
         @Override public void mousePressed    (final MouseEvent      event) {ZoomPane.this.mayShowPopupMenu(event);}
 511  0
         @Override public void mouseReleased   (final MouseEvent      event) {ZoomPane.this.mayShowPopupMenu(event);}
 512  2
         @Override public void componentResized(final ComponentEvent  event) {ZoomPane.this.processSizeEvent(event);}
 513  0
         @Override public void componentMoved  (final ComponentEvent  event) {}
 514  0
         @Override public void componentShown  (final ComponentEvent  event) {}
 515  0
         @Override public void componentHidden (final ComponentEvent  event) {}
 516  
     }
 517  
 
 518  
     /**
 519  
      * Affine transform containing zoom factors, translations and rotations. During the
 520  
      * painting of a component, this affine transform should be combined with a call to
 521  
      * <code>{@linkplain Graphics2D#transform(AffineTransform) Graphics2D.transform}(zoom)</code>.
 522  
      */
 523  5
     protected final AffineTransform zoom = new AffineTransform();
 524  
 
 525  
     /**
 526  
      * Indicates whether the zoom is the result of a {@link #reset} operation.
 527  
      * This is used in order to determine which behavior to replicate when the
 528  
      * widget is resized.
 529  
      */
 530  5
     private boolean zoomIsReset = true;
 531  
 
 532  
     /**
 533  
      * {@code true} if calls to {@link #repaint} should be temporarily disabled.
 534  
      */
 535  
     private boolean disableRepaint;
 536  
 
 537  
     /**
 538  
      * Types of zoom permitted.  This field should be a combination of the constants
 539  
      * {@link #SCALE_X}, {@link #SCALE_Y}, {@link #TRANSLATE_X}, {@link #TRANSLATE_Y},
 540  
      * {@link #ROTATE}, {@link #RESET} and {@link #DEFAULT_ZOOM}.
 541  
      */
 542  
     private final int allowedActions;
 543  
 
 544  
     /**
 545  
      * Strategy to follow in order to calculate the initial affine transform. The value
 546  
      * {@code true} indicates that the content should fill the entire panel, even if it
 547  
      * means losing some of the edges. The value {@code false} indicates, on the contrary,
 548  
      * that we should display the entire contents, even if it means leaving blank spaces in
 549  
      * the panel.
 550  
      */
 551  
     private boolean fillPanel;
 552  
 
 553  
     /**
 554  
      * Rectangle representing the logical coordinates of the visible region. This information is
 555  
      * used to keep the same region when the size or position of the component changes. Initially,
 556  
      * this rectangle is empty. It will only stop being empty if {@link #reset} is called and
 557  
      * {@link #getPreferredArea} and {@link #getZoomableBounds} have both returned valid coordinates.
 558  
      *
 559  
      * @see #getVisibleArea
 560  
      * @see #setVisibleArea
 561  
      */
 562  5
     private final Rectangle2D visibleArea = new Rectangle2D.Double();
 563  
 
 564  
     /**
 565  
      * Rectangle representing the logical coordinates of the region to display initially, the first
 566  
      * time that the window is displayed. The value {@code null} indicates a call to {@link #getArea}.
 567  
      *
 568  
      * @see #getPreferredArea
 569  
      * @see #setPreferredArea
 570  
      */
 571  
     private Rectangle2D preferredArea;
 572  
 
 573  
     /**
 574  
      * Menu to display when the user right clicks with their mouse.
 575  
      * This menu will contain the navigation options.
 576  
      *
 577  
      * @see #getPopupMenu
 578  
      */
 579  
     private transient PointPopupMenu navigationPopupMenu;
 580  
 
 581  
     /**
 582  
      * Flag indicating which part of the paint is in progress.  The permitted values are
 583  
      * {@link #IS_PAINTING}, {@link #IS_PAINTING_MAGNIFIER} and {@link #IS_PRINTING}.
 584  
      */
 585  
     private transient int flag;
 586  
 
 587  
     /**
 588  
      * Indicates if this {@code ZoomPane} object should be repainted when the user adjusts the
 589  
      * scrollbars.  The default value is {@code false}, which means that {@code ZoomPane} will
 590  
      * wait until the user has released the scrollbar before repainting the component.
 591  
      *
 592  
      * @see #isPaintingWhileAdjusting
 593  
      * @see #setPaintingWhileAdjusting
 594  
      */
 595  
     private boolean paintingWhileAdjusting;
 596  
 
 597  
     /**
 598  
      * Rectangle in which to place the coordinates returned by {@link #getZoomableBounds}. This
 599  
      * object is defined in order to avoid allocating objects too often {@link Rectangle}.
 600  
      */
 601  
     private transient Rectangle cachedBounds;
 602  
 
 603  
     /**
 604  
      * Object in which to record the result of {@link #getInsets}. Used in order to avoid
 605  
      * {@link #getZoomableBounds} allocating {@link Insets} objects too often.
 606  
      */
 607  
     private transient Insets cachedInsets;
 608  
 
 609  
     /**
 610  
      * Indicates whether the user is authorised to display the magnifying glass.
 611  
      * The default value is {@code true}.
 612  
      */
 613  5
     private boolean magnifierEnabled = true;
 614  
 
 615  
     /**
 616  
      * Magnification factor inside the magnifying glass. This factor must be greater than 1.
 617  
      */
 618  5
     private double magnifierPower = 4;
 619  
 
 620  
     /**
 621  
      * Geometric shape in which to magnify. The coordinates of this shape should be expressed
 622  
      * in pixels.  The value {@code null} means that no magnifying glass will be drawn.
 623  
      */
 624  
     private transient MouseReshapeTracker magnifier;
 625  
 
 626  
     /**
 627  
      * Colour with which to tint magnifying glass.
 628  
      */
 629  5
     private Paint magnifierGlass = DEFAULT_MAGNIFIER_GLASS;
 630  
 
 631  
     /**
 632  
      * Colour of the magnifying glass's border.
 633  
      */
 634  5
     private Paint magnifierBorder = DEFAULT_MAGNIFIER_BORDER;
 635  
 
 636  
     /**
 637  
      * Constructs a {@code ZoomPane}.
 638  
      *
 639  
      * @param allowedActions
 640  
      *             Allowed zoom actions. It can be a bitwise combination of the following constants:
 641  
      *             {@link #SCALE_X}, {@link #SCALE_Y}, {@link #UNIFORM_SCALE}, {@link #TRANSLATE_X},
 642  
      *             {@link #TRANSLATE_Y}, {@link #ROTATE}, {@link #RESET} and {@link #DEFAULT_ZOOM}.
 643  
      * @throws IllegalArgumentException If {@code type} is invalid.
 644  
      */
 645  5
     public ZoomPane(final int allowedActions) throws IllegalArgumentException {
 646  5
         if ((allowedActions & ~MASK) != 0) {
 647  0
             throw new IllegalArgumentException();
 648  
         }
 649  5
         this.allowedActions = allowedActions;
 650  5
         final Vocabulary resources = Vocabulary.getResources(null);
 651  5
         final InputMap   inputMap = super.getInputMap();
 652  5
         final ActionMap actionMap = super.getActionMap();
 653  55
         for (int i = 0; i < ACTION_ID.length; i++) {
 654  50
             final short actionType = ACTION_TYPE[i];
 655  50
             if ((actionType & allowedActions) != 0) {
 656  44
                 final String  actionID = ACTION_ID[i];
 657  44
                 final double    amount = ACTION_AMOUNT[i];
 658  44
                 final int     keyboard = ACTION_KEY[(i << 1) + 0];
 659  44
                 final int     modifier = ACTION_KEY[(i << 1) + 1];
 660  44
                 final KeyStroke stroke = KeyStroke.getKeyStroke(keyboard, modifier);
 661  44
                 final Action    action = new AbstractAction() {
 662  
                     /*
 663  
                      * Action to perform when a key has been hit or the mouse clicked.
 664  
                      */
 665  
                     @Override
 666  
                     public void actionPerformed(final ActionEvent event) {
 667  0
                         Point point = null;
 668  0
                         final Object  source = event.getSource();
 669  0
                         final boolean button = (source instanceof AbstractButton);
 670  0
                         if (button) {
 671  0
                             for (Container c = (Container) source; c != null; c = c.getParent()) {
 672  0
                                 if (c instanceof PointPopupMenu) {
 673  0
                                     point = ((PointPopupMenu) c).point;
 674  0
                                     break;
 675  
                                 }
 676  
                             }
 677  
                         }
 678  0
                         double m = amount;
 679  0
                         if (button || (event.getModifiers() & ActionEvent.SHIFT_MASK) != 0) {
 680  0
                             if ((actionType & UNIFORM_SCALE) != 0) {
 681  0
                                 m = (m >= 1) ? 2.0 : 0.5;
 682  
                             }
 683  
                             else {
 684  0
                                 m *= ENHANCEMENT_FACTOR;
 685  
                             }
 686  
                         }
 687  0
                         transform(actionType & allowedActions, m, point);
 688  0
                     }
 689  
                 };
 690  44
                 action.putValue(Action.NAME, resources.getString(RESOURCE_ID[i]));
 691  44
                 action.putValue(Action.ACTION_COMMAND_KEY, actionID);
 692  44
                 action.putValue(Action.ACCELERATOR_KEY, stroke);
 693  44
                 actionMap.put(actionID, action);
 694  44
                 inputMap .put(stroke, actionID);
 695  44
                 inputMap .put(KeyStroke.getKeyStroke(keyboard, modifier | KeyEvent.SHIFT_MASK), actionID);
 696  
             }
 697  
         }
 698  
         /*
 699  
          * Adds an object which will be in charge of listening for mouse clicks in order to
 700  
          * display a contextual menu, as well as an object which will be in charge of listening
 701  
          * for mouse movements in order to perform zooms.
 702  
          */
 703  5
         final Listeners listeners = new Listeners();
 704  5
         super.addComponentListener(listeners);
 705  5
         super.addMouseListener(listeners);
 706  5
         if ((allowedActions & (SCALE_X | SCALE_Y)) != 0) {
 707  5
             super.addMouseWheelListener(listeners);
 708  
         }
 709  5
         super.addMouseListener(mouseSelectionTracker);
 710  5
         super.setBackground(Color.WHITE);
 711  5
         super.setAutoscrolls(true);
 712  5
         super.setFocusable(true);
 713  5
         super.setOpaque(true);
 714  5
         super.setUI(UI);
 715  5
     }
 716  
 
 717  
     /**
 718  
      * Reinitializes the {@linkplain #zoom zoom} affine transform in order to cancel any zoom,
 719  
      * rotation or translation. The default implementation performs the initialisation in such
 720  
      * a way that the <var>y</var> axis point upwards and make the whole of the region covered
 721  
      * by the {@link #getPreferredArea() getPreferredArea()} logical coordinates appears in the
 722  
      * panel.
 723  
      *
 724  
      * {@note <code>reset()</code> is <u>the only</u> method of <code>ZoomPane</code> which doesn't
 725  
      * have to pass through the <code>transform(AffineTransform)</code> method to modify the zoom.
 726  
      * This exception is necessary to avoid falling into an infinite loop.}
 727  
      */
 728  
     public void reset() {
 729  3
         reset(getZoomableBounds(), true);
 730  3
     }
 731  
 
 732  
     /**
 733  
      * Reinitializes the affine transform {@link #zoom zoom} in order to cancel any zoom, rotation or
 734  
      * translation. The argument {@code yAxisUpward} indicates whether the <var>y</var> axis should
 735  
      * point upwards. The value {@code false} lets it point downwards. This method is offered
 736  
      * for convenience sake for derived classes which want to redefine {@link #reset()}.
 737  
      *
 738  
      * @param zoomableBounds Coordinates, in pixels, of the screen space in which to draw.
 739  
      *        This argument will usually be
 740  
      *        <code>{@link #getZoomableBounds(Rectangle) getZoomableBounds}(null)</code>.
 741  
      * @param yAxisUpward {@code true} if the <var>y</var> axis should point upwards rather than
 742  
      *        downwards.
 743  
      */
 744  
     protected void reset(final Rectangle zoomableBounds, final boolean yAxisUpward) {
 745  5
         if (!zoomableBounds.isEmpty()) {
 746  5
             final Rectangle2D preferredArea = getPreferredArea();
 747  5
             if (isValid(preferredArea)) {
 748  
                 final AffineTransform change;
 749  
                 try {
 750  3
                     change = zoom.createInverse();
 751  0
                 } catch (NoninvertibleTransformException exception) {
 752  0
                     unexpectedException("reset", exception);
 753  0
                     return;
 754  3
                 }
 755  3
                 if (yAxisUpward) {
 756  3
                     zoom.setToScale(+1, -1);
 757  
                 } else {
 758  0
                     zoom.setToIdentity();
 759  
                 }
 760  3
                 final AffineTransform transform = setVisibleArea(preferredArea, zoomableBounds,
 761  
                         SCALE_X | SCALE_Y | TRANSLATE_X | TRANSLATE_Y);
 762  3
                 change.concatenate(zoom);
 763  3
                 zoom  .concatenate(transform);
 764  3
                 change.concatenate(transform);
 765  3
                 getVisibleArea(zoomableBounds); // Force update of 'visibleArea'
 766  
                 /*
 767  
                  * The three private versions 'fireZoomPane0', 'getVisibleArea'
 768  
                  * and 'setVisibleArea' avoid calling other methods of ZoomPane
 769  
                  * so as not to end up in an infinite loop.
 770  
                  */
 771  3
                 if (!change.isIdentity()) {
 772  3
                     fireZoomChanged0(change);
 773  3
                     if (!disableRepaint) {
 774  2
                         repaint(zoomableBounds);
 775  
                     }
 776  
                 }
 777  3
                 zoomIsReset = true;
 778  3
                 log("reset", visibleArea);
 779  
             }
 780  
         }
 781  5
     }
 782  
 
 783  
     /**
 784  
      * Indicates whether the zoom is the result of a {@link #reset} operation.
 785  
      */
 786  
     final boolean zoomIsReset() {
 787  2
         return zoomIsReset;
 788  
     }
 789  
 
 790  
     /**
 791  
      * Sets the policy for the zoom when the content is initially drawn or when the user resets the
 792  
      * zoom. Value {@code true} means that the panel should initially be completely filled, even if
 793  
      * the content partially falls outside the panel's bounds. Value {@code false} means that the
 794  
      * full content should appear in the panel, even if some space is not used. Default value is
 795  
      * {@code false}.
 796  
      *
 797  
      * @param fill {@code true} if the panel should be initially completely filled.
 798  
      */
 799  
     protected void setResetPolicy(final boolean fill) {
 800  2
         fillPanel = fill;
 801  2
     }
 802  
 
 803  
     /**
 804  
      * Returns a bounding box that contains the logical coordinates of all data that may be displayed
 805  
      * in this {@code ZoomPane}. For example, if this {@code ZoomPane} is to display a geographic map,
 806  
      * then this method should return the map's bounds in degrees of latitude and longitude (if the
 807  
      * underlying CRS is {@linkplain org.opengis.referencing.crs.GeographicCRS geographic}), in metres
 808  
      * (if the underlying CRS is {@linkplain org.opengis.referencing.crs.ProjectedCRS projected}) or
 809  
      * some other geodetic units. This bounding box is completely independent of any current zoom
 810  
      * setting and will change only if the content changes.
 811  
      *
 812  
      * @return A bounding box for the logical coordinates of all contents that are going to be
 813  
      *         drawn in this {@code ZoomPane}. If this bounding box is unknown, then this method
 814  
      *         can return {@code null} (but this is not recommended).
 815  
      */
 816  
     public abstract Rectangle2D getArea();
 817  
 
 818  
     /**
 819  
      * Indicates whether the logical coordinates of a region have been defined. This method returns
 820  
      * {@code true} if {@link #setPreferredArea} has been called with a non null argument.
 821  
      *
 822  
      * @return {@code true} if a preferred area has been set.
 823  
      */
 824  
     public final boolean hasPreferredArea() {
 825  0
         return preferredArea != null;
 826  
     }
 827  
 
 828  
     /**
 829  
      * Returns the logical coordinates of the region that we want to see displayed the first time
 830  
      * that {@code ZoomPane} appears on the screen.  This region will also be displayed each time
 831  
      * the method {@link #reset} is called. The default implementation goes as follows:
 832  
      * <p>
 833  
      * <ul>
 834  
      *   <li>If a region has already been defined by a call to
 835  
      *       {@link #setPreferredArea}, this region will be returned.</li>
 836  
      *   <li>If not, the whole region {@link #getArea} will be returned.</li>
 837  
      * </ul>
 838  
      *
 839  
      * @return The logical coordinates of the region to be initially displayed,
 840  
      *         or {@code null} if these coordinates are unknown.
 841  
      */
 842  
     public final Rectangle2D getPreferredArea() {
 843  5
         return (preferredArea != null) ? (Rectangle2D) preferredArea.clone() : getArea();
 844  
     }
 845  
 
 846  
     /**
 847  
      * Specifies the logical coordinates of the region that we want to see displayed the first time
 848  
      * that {@code ZoomPane} appears on the screen. This region will also be displayed the first
 849  
      * time that the {@link #reset} method is called.
 850  
      *
 851  
      * @param area The logical coordinates of the region to be initially displayed,
 852  
      */
 853  
     public final void setPreferredArea(final Rectangle2D area) {
 854  0
         if (area != null) {
 855  0
             if (isValid(area)) {
 856  
                 final Object oldArea;
 857  0
                 if (preferredArea == null) {
 858  0
                     oldArea = null;
 859  0
                     preferredArea = new Rectangle2D.Double();
 860  
                 }
 861  0
                 else oldArea = preferredArea.clone();
 862  0
                 preferredArea.setRect(area);
 863  0
                 firePropertyChange("preferredArea", oldArea, area);
 864  0
                 log("setPreferredArea", area);
 865  0
             } else {
 866  0
                 throw new IllegalArgumentException(Errors.format(Errors.Keys.EMPTY_RECTANGLE_$1, area));
 867  
             }
 868  
         }
 869  0
         else preferredArea = null;
 870  0
     }
 871  
 
 872  
     /**
 873  
      * Returns the logical coordinates of the region visible on the screen. In the case of a
 874  
      * geographic map, for example, the logical coordinates can be expressed in degrees of
 875  
      * latitude/longitude or in metres if a cartographic projection has been defined.
 876  
      *
 877  
      * @return The region visible on the screen, in logical coordinates.
 878  
      */
 879  
     public final Rectangle2D getVisibleArea() {
 880  0
         return getVisibleArea(getZoomableBounds());
 881  
     }
 882  
 
 883  
     /**
 884  
      * Implementation of {@link #getVisibleArea()}.
 885  
      */
 886  
     private Rectangle2D getVisibleArea(final Rectangle zoomableBounds) {
 887  3
         if (zoomableBounds.isEmpty()) {
 888  0
             return (Rectangle2D) visibleArea.clone();
 889  
         }
 890  
         Rectangle2D visible;
 891  
         try {
 892  3
             visible = XAffineTransform.inverseTransform(zoom, zoomableBounds, null);
 893  0
         } catch (NoninvertibleTransformException exception) {
 894  0
             unexpectedException("getVisibleArea", exception);
 895  0
             visible = new Rectangle2D.Double(zoomableBounds.getCenterX(),
 896  
                                              zoomableBounds.getCenterY(), 0, 0);
 897  3
         }
 898  3
         visibleArea.setRect(visible);
 899  3
         return visible;
 900  
     }
 901  
 
 902  
     /**
 903  
      * Defines the limits of the visible part, in logical coordinates.  This method will modify the
 904  
      * zoom and the translation in order to display the specified region. If {@link #zoom} contains
 905  
      * a rotation, this rotation will not be modified.
 906  
      *
 907  
      * @param  logicalBounds Logical coordinates of the region to be displayed.
 908  
      * @throws IllegalArgumentException if {@code source} is empty.
 909  
      */
 910  
     public void setVisibleArea(final Rectangle2D logicalBounds) throws IllegalArgumentException {
 911  0
         log("setVisibleArea", logicalBounds);
 912  0
         transform(setVisibleArea(logicalBounds, getZoomableBounds(), 0));
 913  0
     }
 914  
 
 915  
     /**
 916  
      * Defines the limits of the visible part, in logical coordinates.  This method will modify the
 917  
      * zoom and the translation in order to display the specified region. If {@link #zoom} contains
 918  
      * a rotation, this rotation will not be modified.
 919  
      *
 920  
      * @param  source Logical coordinates of the region to be displayed.
 921  
      * @param  dest Pixel coordinates of the region of the window in which to
 922  
      *         draw (normally {@link #getZoomableBounds()}).
 923  
      * @param  mask A mask to {@code OR} with the {@link #allowedActions} for determining which
 924  
      *         kind of transformation are allowed. The {@link #allowedActions} is not modified.
 925  
      * @return Change to apply to the affine transform {@link #zoom}.
 926  
      * @throws IllegalArgumentException if {@code source} is empty.
 927  
      */
 928  
     private AffineTransform setVisibleArea(Rectangle2D source, Rectangle2D dest, int mask)
 929  
             throws IllegalArgumentException
 930  
     {
 931  
         /*
 932  
          * Verifies the validity of the source rectangle. An invalid rectangle will be rejected.
 933  
          * However, we will be more flexible for dest since the window could have been reduced by
 934  
          * the user.
 935  
          */
 936  3
         if (!isValid(source)) {
 937  0
             throw new IllegalArgumentException(Errors.format(Errors.Keys.EMPTY_RECTANGLE_$1, source));
 938  
         }
 939  3
         if (!isValid(dest)) {
 940  0
             return new AffineTransform();
 941  
         }
 942  
         /*
 943  
          * Converts the destination into logical coordinates.  We can then perform
 944  
          * a zoom and a translation which would put {@code source} in {@code dest}.
 945  
          */
 946  
         try {
 947  3
             dest = XAffineTransform.inverseTransform(zoom, dest, null);
 948  0
         } catch (NoninvertibleTransformException exception) {
 949  0
             unexpectedException("setVisibleArea", exception);
 950  0
             return new AffineTransform();
 951  3
         }
 952  3
         final double sourceWidth  = source.getWidth ();
 953  3
         final double sourceHeight = source.getHeight();
 954  3
         final double destWidth    =   dest.getWidth ();
 955  3
         final double destHeight   =   dest.getHeight();
 956  3
         double sx = destWidth / sourceWidth;
 957  3
         double sy = destHeight / sourceHeight;
 958  
         /*
 959  
          * Standardizes the horizontal and vertical scales,
 960  
          * if such a standardization has been requested.
 961  
          */
 962  3
         mask |= allowedActions;
 963  3
         if ((mask & UNIFORM_SCALE) == UNIFORM_SCALE) {
 964  1
             if (fillPanel) {
 965  0
                 if (sy * sourceWidth  > destWidth ) {
 966  0
                     sx = sy;
 967  0
                 } else if (sx * sourceHeight > destHeight) {
 968  0
                     sy = sx;
 969  
                 }
 970  
             } else {
 971  1
                 if (sy * sourceWidth  < destWidth ) {
 972  0
                     sx = sy;
 973  1
                 } else if (sx * sourceHeight < destHeight) {
 974  1
                     sy = sx;
 975  
                 }
 976  
             }
 977  
         }
 978  3
         final AffineTransform change = AffineTransform.getTranslateInstance(
 979  
                          (mask & TRANSLATE_X) != 0 ? dest.getCenterX()    : 0,
 980  
                          (mask & TRANSLATE_Y) != 0 ? dest.getCenterY()    : 0);
 981  3
         change.scale    ((mask & SCALE_X    ) != 0 ? sx                   : 1,
 982  
                          (mask & SCALE_Y    ) != 0 ? sy                   : 1);
 983  3
         change.translate((mask & TRANSLATE_X) != 0 ? -source.getCenterX() : 0,
 984  
                          (mask & TRANSLATE_Y) != 0 ? -source.getCenterY() : 0);
 985  3
         XAffineTransform.roundIfAlmostInteger(change, EPS);
 986  3
         return change;
 987  
     }
 988  
 
 989  
     /**
 990  
      * Returns the bounding box (in pixel coordinates) of the zoomable area.
 991  
      * <strong>For performance reasons, this method reuses an internal cache.
 992  
      * Never modify the returned rectangle!</strong>. This internal method
 993  
      * is invoked by every method looking for this {@code ZoomPane}
 994  
      * dimension.
 995  
      *
 996  
      * @return The bounding box of the zoomable area, in pixel coordinates
 997  
      *         relative to this {@code ZoomPane} widget. <strong>Do not
 998  
      *         change the returned rectangle!</strong>
 999  
      */
 1000  
     private Rectangle getZoomableBounds() {
 1001  3
         return cachedBounds = getZoomableBounds(cachedBounds);
 1002  
     }
 1003  
 
 1004  
     /**
 1005  
      * Returns the bounding box (in pixel coordinates) of the zoomable area. This method is similar
 1006  
      * to {@link #getBounds(Rectangle)}, except that the zoomable area may be smaller than the whole
 1007  
      * widget area. For example, a chart needs to keep some space for axes around the zoomable area.
 1008  
      * Another difference is that pixel coordinates are relative to the widget, i.e. the (0,0)
 1009  
      * coordinate lies on the {@code ZoomPane} upper left corner, no matter what its location on
 1010  
      * screen.
 1011  
      * <p>
 1012  
      * {@code ZoomPane} invokes {@code getZoomableBounds} when it needs to set up an initial
 1013  
      * {@link #zoom} value. Subclasses should also set the clip area to this bounding box in their
 1014  
      * {@link #paintComponent(Graphics2D)} method <em>before</em> setting the graphics transform.
 1015  
      * For example:
 1016  
      *
 1017  
      * {@preformat java
 1018  
      *     graphics.clip(getZoomableBounds(null));
 1019  
      *     graphics.transform(zoom);
 1020  
      * }
 1021  
      *
 1022  
      * @param  bounds An optional pre-allocated rectangle, or {@code null} to create a new one. This
 1023  
      *         argument is useful if the caller wants to avoid allocating a new object on the heap.
 1024  
      * @return The bounding box of the zoomable area, in pixel coordinates
 1025  
      *         relative to this {@code ZoomPane} widget.
 1026  
      */
 1027  
     protected Rectangle getZoomableBounds(Rectangle bounds) {
 1028  
         Insets insets;
 1029  6
         bounds = getBounds(bounds); insets = cachedInsets;
 1030  6
         insets = getInsets(insets); cachedInsets = insets;
 1031  6
         if (bounds.isEmpty()) {
 1032  3
             final Dimension size = getPreferredSize();
 1033  3
             bounds.width  = size.width;
 1034  3
             bounds.height = size.height;
 1035  
         }
 1036  6
         bounds.x       =  insets.left;
 1037  6
         bounds.y       =  insets.top;
 1038  6
         bounds.width  -= (insets.left + insets.right);
 1039  6
         bounds.height -= (insets.top + insets.bottom);
 1040  6
         return bounds;
 1041  
     }
 1042  
 
 1043  
     /**
 1044  
      * Returns the default size for this component.  This is the size returned by
 1045  
      * {@link #getPreferredSize} if no preferred size has been explicitly set with
 1046  
      * {@link #setPreferredSize}.
 1047  
      *
 1048  
      * @return The default size for this component.
 1049  
      */
 1050  
     protected Dimension getDefaultSize() {
 1051  7
         return getViewSize();
 1052  
     }
 1053  
 
 1054  
     /**
 1055  
      * Returns the preferred pixel size for a close zoom. For image rendering, the preferred pixel
 1056  
      * size is the image's pixel size in logical units. For other kinds of rendering, this "pixel"
 1057  
      * size should be some reasonable resolution. The default implementation computes a default
 1058  
      * value from {@link #getArea}.
 1059  
      *
 1060  
      * @return The preferred pixel size for a close zoom, in logical units.
 1061  
      */
 1062  
     protected Dimension2D getPreferredPixelSize() {
 1063  0
         final Rectangle2D area = getArea();
 1064  0
         if (isValid(area)) {
 1065  0
             final double sx = area.getWidth () / (10 * getWidth ());
 1066  0
             final double sy = area.getHeight() / (10 * getHeight());
 1067  0
             return new DoubleDimension2D(sx, sy);
 1068  
         } else {
 1069  0
             return new Dimension(1, 1);
 1070  
         }
 1071  
     }
 1072  
 
 1073  
     /**
 1074  
      * Returns the current {@linkplain #zoom} scale factor. A value of 1/100 means that 100 metres
 1075  
      * are displayed as 1 pixel (assuming that the logical coordinates of {@link #getArea} are
 1076  
      * expressed in metres). Scale factors for X and Y axes can be computed separately using the
 1077  
      * following equations:
 1078  
      * <p>
 1079  
      * <table align="center" width="600" cellspacing="3"><tr>
 1080  
      * <td width=50%>X scale = <IMG src="../../referencing/operation/matrix/doc-files/scaleX0.png"></td>
 1081  
      * <td width=50%>Y scale = <IMG src="../../referencing/operation/matrix/doc-files/scaleY0.png"></td>
 1082  
      * </tr></table>
 1083  
      * <p>
 1084  
      * This method combines scale along both axes, which is correct if this {@code ZoomPane} has
 1085  
      * been constructed with the {@link #UNIFORM_SCALE} type.
 1086  
      *
 1087  
      * @return The current scale factor calculated from the {@link #zoom} affine transform.
 1088  
      */
 1089  
     public double getScaleFactor() {
 1090  0
         return XAffineTransform.getScale(zoom);
 1091  
     }
 1092  
 
 1093  
     /**
 1094  
      * Returns a clone of the current {@link #zoom} transform.
 1095  
      *
 1096  
      * @return A clone of the current transform.
 1097  
      *
 1098  
      * @since 3.00
 1099  
      */
 1100  
     public AffineTransform getTransform() {
 1101  0
         return new AffineTransform(zoom);
 1102  
     }
 1103  
 
 1104  
     /**
 1105  
      * Sets the {@link #zoom} transform to the given value. The default implementation computes an
 1106  
      * affine transform which is the change needed for going from the current {@linkplain #zoom}
 1107  
      * to the given transform, then calls {@link #transform(AffineTransform)} with that change.
 1108  
      * This is done that way for giving listeners a chance to track the changes.
 1109  
      *
 1110  
      * @param tr The new transform.
 1111  
      *
 1112  
      * @since 3.00
 1113  
      */
 1114  
     public void setTransform(final AffineTransform tr) {
 1115  
         final AffineTransform change;
 1116  
         try {
 1117  0
             change = zoom.createInverse();
 1118  0
         } catch (NoninvertibleTransformException exception) {
 1119  
             // Note: we won't be able to invoke fireZoomChanged since we can't compute the change.
 1120  0
             Logging.unexpectedException(LOGGER, ZoomPane.class, "setTransform", exception);
 1121  0
             zoom.setTransform(tr);
 1122  0
             return;
 1123  0
         }
 1124  0
         change.concatenate(tr);
 1125  0
         XAffineTransform.roundIfAlmostInteger(change, EPS);
 1126  0
         transform(change);
 1127  0
     }
 1128  
 
 1129  
     /**
 1130  
      * Changes the {@linkplain #zoom zoom} by applying an affine transform. The {@code change}
 1131  
      * transform must express a change in logical units, for example, a translation in metres.
 1132  
      * This method is conceptually similar to the following code:
 1133  
      *
 1134  
      * {@preformat java
 1135  
      *     zoom.concatenate(change);
 1136  
      *     fireZoomChanged(change);
 1137  
      *     repaint(getZoomableBounds(null));
 1138  
      * }
 1139  
      *
 1140  
      * @param  change The zoom change, as an affine transform in logical coordinates. If
 1141  
      *         {@code change} is the identity transform, then this method does nothing and
 1142  
      *         listeners are not notified.
 1143  
      */
 1144  
     public void transform(final AffineTransform change) {
 1145  0
         if (!change.isIdentity()) {
 1146  0
             zoom.concatenate(change);
 1147  0
             XAffineTransform.roundIfAlmostInteger(zoom, EPS);
 1148  0
             fireZoomChanged(change);
 1149  0
             if (!disableRepaint) {
 1150  0
                 repaint(getZoomableBounds());
 1151  
             }
 1152  0
             zoomIsReset = false;
 1153  
         }
 1154  0
     }
 1155  
 
 1156  
     /**
 1157  
      * Changes the {@linkplain #zoom} by applying an affine transform. The {@code change} transform
 1158  
      * must express a change in pixel units, for example, a scrolling of 6 pixels toward right. This
 1159  
      * method is conceptually similar to the following code:
 1160  
      *
 1161  
      * {@preformat java
 1162  
      *     zoom.preConcatenate(change);
 1163  
      *     // Converts the change from pixel to logical units
 1164  
      *     AffineTransform logical = zoom.createInverse();
 1165  
      *     logical.concatenate(change);
 1166  
      *     logical.concatenate(zoom);
 1167  
      *     fireZoomChanged(logical);
 1168  
      *     repaint(getZoomableBounds(null));
 1169  
      * }
 1170  
      *
 1171  
      * @param  change The zoom change, as an affine transform in pixel coordinates. If
 1172  
      *         {@code change} is the identity transform, then this method does nothing
 1173  
      *         and listeners are not notified.
 1174  
      *
 1175  
      * @since 2.1
 1176  
      */
 1177  
     public void transformPixels(final AffineTransform change) {
 1178  0
         if (!change.isIdentity()) {
 1179  
             final AffineTransform logical;
 1180  
             try {
 1181  0
                 logical = zoom.createInverse();
 1182  0
             } catch (NoninvertibleTransformException exception) {
 1183  0
                 throw new IllegalStateException(exception);
 1184  0
             }
 1185  0
             logical.concatenate(change);
 1186  0
             logical.concatenate(zoom);
 1187  0
             XAffineTransform.roundIfAlmostInteger(logical, EPS);
 1188  0
             transform(logical);
 1189  
         }
 1190  0
     }
 1191  
 
 1192  
     /**
 1193  
      * Carries out a zoom, a translation or a rotation on the contents of {@code ZoomPane}. The
 1194  
      * type of operation to carry out depends on the {@code operation} argument:
 1195  
      * <p>
 1196  
      * <ul>
 1197  
      *   <li>{@link #TRANSLATE_X} carries out a translation along the <var>x</var> axis.
 1198  
      *       The {@code amount} argument specifies the transformation to perform in number
 1199  
      *       of pixels. A negative value moves to the left whilst a positive value moves to
 1200  
      *       the right.</li>
 1201  
      *   <li>{@link #TRANSLATE_Y} carries out a translation along the <var>y</var> axis. The
 1202  
      *       {@code amount} argument specifies the transformation to perform in number of pixels.
 1203  
      *       A negative valuemoves upwards whilst a positive value moves downwards.</li>
 1204  
      *   <li>{@link #UNIFORM_SCALE} carries out a zoom. The {@code amount} argument specifies the
 1205  
      *       type of zoom to perform. A value greater than 1 will perform a zoom in whilst a value
 1206  
      *       between 0 and 1 will perform a zoom out.</li>
 1207  
      *   <li>{@link #ROTATE} carries out a rotation. The {@code amount} argument specifies the
 1208  
      *       rotation angle in radians.</li>
 1209  
      *   <li>{@link #RESET} Redefines the zoom to a default scale, rotation and translation. This
 1210  
      *       operation displays all, or almost all, the contents of {@code ZoomPane}.</li>
 1211  
      *   <li>{@link #DEFAULT_ZOOM} Carries out a default zoom, close to the maximum zoom, which
 1212  
      *       shows the details of the contents of {@code ZoomPane} but without enlarging them too
 1213  
      *       much.</li>
 1214  
      * </ul>
 1215  
      *
 1216  
      * @param  operation Type of operation to perform.
 1217  
      * @param  amount ({@link #TRANSLATE_X} and {@link #TRANSLATE_Y}) translation in pixels,
 1218  
      *         ({@link #SCALE_X} and {@link #SCALE_Y}) scale factor or ({@link #ROTATE}) rotation
 1219  
      *         angle in radians. In other cases, this argument is ignored and can be {@link Double#NaN}.
 1220  
      * @param  center Zoom centre ({@link #SCALE_X} and {@link #SCALE_Y}) or rotation centre
 1221  
      *         ({@link #ROTATE}), in pixel coordinates. The value {@code null} indicates a default
 1222  
      *         value, more often not the centre of the window.
 1223  
      * @throws UnsupportedOperationException if the {@code operation} argument isn't recognized.
 1224  
      */
 1225  
     private void transform(final int operation, final double amount, final Point2D center)
 1226  
             throws UnsupportedOperationException
 1227  
     {
 1228  0
         if ((operation & (RESET)) != 0) {
 1229  
             /////////////////////
 1230  
             ////    RESET    ////
 1231  
             /////////////////////
 1232  0
             if ((operation & ~(RESET)) != 0) {
 1233  0
                 throw new UnsupportedOperationException();
 1234  
             }
 1235  0
             reset();
 1236  0
             return;
 1237  
         }
 1238  
         final AffineTransform change;
 1239  
         try {
 1240  0
             change = zoom.createInverse();
 1241  0
         } catch (NoninvertibleTransformException exception) {
 1242  0
             unexpectedException("transform", exception);
 1243  0
             return;
 1244  0
         }
 1245  0
         if ((operation & (TRANSLATE_X | TRANSLATE_Y)) != 0) {
 1246  
             /////////////////////////
 1247  
             ////    TRANSLATE    ////
 1248  
             /////////////////////////
 1249  0
             if ((operation & ~(TRANSLATE_X | TRANSLATE_Y)) != 0) {
 1250  0
                 throw new UnsupportedOperationException();
 1251  
             }
 1252  0
             change.translate(((operation & TRANSLATE_X) != 0) ? amount : 0,
 1253  
                              ((operation & TRANSLATE_Y) != 0) ? amount : 0);
 1254  
         } else {
 1255  
             /*
 1256  
              * Obtains the coordinates (in pixels) of the rotation or zoom centre.
 1257  
              */
 1258  
             final double centerX;
 1259  
             final double centerY;
 1260  0
             if (center != null) {
 1261  0
                 centerX = center.getX();
 1262  0
                 centerY = center.getY();
 1263  
             } else {
 1264  0
                 final Rectangle bounds = getZoomableBounds();
 1265  0
                 if (bounds.width >= 0 && bounds.height >= 0) {
 1266  0
                     centerX = bounds.getCenterX();
 1267  0
                     centerY = bounds.getCenterY();
 1268  
                 } else {
 1269  0
                     return;
 1270  
                 }
 1271  
                 /*
 1272  
                  * Zero lengths and widths are accepted.  If, however, the rectangle isn't valid
 1273  
                  * (negative length or width) then the method will end without doing anything. No
 1274  
                  * zoom will be performed.
 1275  
                  */
 1276  
             }
 1277  0
             if ((operation & (ROTATE)) != 0) {
 1278  
                 //////////////////////
 1279  
                 ////    ROTATE    ////
 1280  
                 //////////////////////
 1281  0
                 if ((operation & ~(ROTATE)) != 0) {
 1282  0
                     throw new UnsupportedOperationException();
 1283  
                 }
 1284  0
                 change.rotate(amount, centerX, centerY);
 1285  0
             } else if ((operation & (SCALE_X | SCALE_Y)) != 0) {
 1286  
                 /////////////////////
 1287  
                 ////    SCALE    ////
 1288  
                 /////////////////////
 1289  0
                 if ((operation & ~(UNIFORM_SCALE)) != 0) {
 1290  0
                     throw new UnsupportedOperationException();
 1291  
                 }
 1292  0
                 change.translate(+centerX, +centerY);
 1293  0
                 change.scale(((operation & SCALE_X) != 0) ? amount : 1,
 1294  
                              ((operation & SCALE_Y) != 0) ? amount : 1);
 1295  0
                 change.translate(-centerX, -centerY);
 1296  0
             } else if ((operation & (DEFAULT_ZOOM)) != 0) {
 1297  
                 ////////////////////////////
 1298  
                 ////    DEFAULT_ZOOM    ////
 1299  
                 ////////////////////////////
 1300  0
                 if ((operation & ~(DEFAULT_ZOOM)) != 0) {
 1301  0
                     throw new UnsupportedOperationException();
 1302  
                 }
 1303  0
                 final Dimension2D size = getPreferredPixelSize();
 1304  0
                 double sx = 1 / (size.getWidth()  * XAffineTransform.getScaleX0(zoom));
 1305  0
                 double sy = 1 / (size.getHeight() * XAffineTransform.getScaleY0(zoom));
 1306  0
                 if ((allowedActions & UNIFORM_SCALE) == UNIFORM_SCALE) {
 1307  0
                     if (sx > sy) sx = sy;
 1308  0
                     if (sy > sx) sy = sx;
 1309  
                 }
 1310  0
                 if ((allowedActions & SCALE_X) == 0) sx = 1;
 1311  0
                 if ((allowedActions & SCALE_Y) == 0) sy = 1;
 1312  0
                 change.translate(+centerX, +centerY);
 1313  0
                 change.scale    ( sx     ,  sy     );
 1314  0
                 change.translate(-centerX, -centerY);
 1315  0
             } else {
 1316  0
                 throw new UnsupportedOperationException();
 1317  
             }
 1318  
         }
 1319  0
         change.concatenate(zoom);
 1320  0
         XAffineTransform.roundIfAlmostInteger(change, EPS);
 1321  0
         transform(change);
 1322  0
     }
 1323  
 
 1324  
     /**
 1325  
      * Adds an object to the list of objects interested in being notified about zoom changes.
 1326  
      *
 1327  
      * @param listener The change listener to add.
 1328  
      */
 1329  
     public void addZoomChangeListener(final ZoomChangeListener listener) {
 1330  0
         listenerList.add(ZoomChangeListener.class, listener);
 1331  0
     }
 1332  
 
 1333  
     /**
 1334  
      * Removes an object from the list of objects interested in being notified about zoom changes.
 1335  
      *
 1336  
      * @param listener The change listener to remove.
 1337  
      */
 1338  
     public void removeZoomChangeListener(final ZoomChangeListener listener) {
 1339  0
         listenerList.remove(ZoomChangeListener.class, listener);
 1340  0
     }
 1341  
 
 1342  
     /**
 1343  
      * Adds an object to the list of objects interested in being notified about mouse events.
 1344  
      *
 1345  
      * @param listener The mouse listener to add.
 1346  
      */
 1347  
     @Override
 1348  
     public void addMouseListener(final MouseListener listener) {
 1349  0
         super.removeMouseListener(mouseSelectionTracker);
 1350  0
         super.addMouseListener   (listener);
 1351  0
         super.addMouseListener   (mouseSelectionTracker); // MUST be last!
 1352  0
     }
 1353  
 
 1354  
     /**
 1355  
      * Signals that a zoom change has taken place. Every object registered by the
 1356  
      * {@link #addZoomChangeListener(ZoomChangeListener)} method will be notified
 1357  
      * of the change as soon as possible.
 1358  
      * <p>
 1359  
      * If {@code oldZoom} and {@code newZoom} are the affine transforms of the old and new zoom
 1360  
      * respectively, the change is computed in such a way that the following relation is respected
 1361  
      * within rounding errors:
 1362  
      *
 1363  
      * {@preformat java
 1364  
      *     newZoom = oldZoom.concatenate(change)
 1365  
      * }
 1366  
      *
 1367  
      * <strong>Note: This method may modify the given {@code change} transform</strong> to
 1368  
      * combine several consecutive calls of {@code fireZoomChanged} in a single transformation.
 1369  
      *
 1370  
      * @param change Affine transform which represents the change in the zoom.
 1371  
      *        The value of this argument may be changed by this method call.
 1372  
      */
 1373  
     protected void fireZoomChanged(final AffineTransform change) {
 1374  0
         visibleArea.setRect(getVisibleArea());
 1375  0
         fireZoomChanged0(change);
 1376  0
     }
 1377  
 
 1378  
     /**
 1379  
      * Notifies derived classes that the zoom has changed. Unlike the protected
 1380  
      * {@link #fireZoomChanged} method, this private method doesn't modify any internal field and
 1381  
      * doesn't attempt to call other {@code ZoomPane} methods such as {@link #getVisibleArea}. An
 1382  
      * infinite loop is thereby avoided as this method is called by {@link #reset}.
 1383  
      */
 1384  
     private void fireZoomChanged0(final AffineTransform change) {
 1385  
         /*
 1386  
          * Note: the event must be fired even if the transformation is the identity matrix,
 1387  
          *       because certain classes use this to update scrollbars.
 1388  
          */
 1389  3
         if (change == null) {
 1390  0
             throw new NullArgumentException();
 1391  
         }
 1392  3
         ZoomChangeEvent event = null;
 1393  3
         final Object[] listeners = listenerList.getListenerList();
 1394  3
         for (int i = listeners.length; (i -= 2) >= 0;) {
 1395  0
             if (listeners[i] == ZoomChangeListener.class) {
 1396  0
                 if (event == null) {
 1397  0
                     event = new ZoomChangeEvent(this, change);
 1398  
                 }
 1399  
                 try {
 1400  0
                     ((ZoomChangeListener) listeners[i+1]).zoomChanged(event);
 1401  0
                 } catch (RuntimeException exception) {
 1402  0
                     unexpectedException("fireZoomChanged", exception);
 1403  0
                 }
 1404  
             }
 1405  
         }
 1406  3
     }
 1407  
 
 1408  
     /**
 1409  
      * Method called automatically after the user selects an area with the mouse. The default
 1410  
      * implementation zooms to the selected {@code area}. Derived classes can redefine this method
 1411  
      * in order to carry out another action.
 1412  
      *
 1413  
      * @param area Area selected by the user, in logical coordinates.
 1414  
      */
 1415  
     protected void mouseSelectionPerformed(final Shape area) {
 1416  0
         final Rectangle2D rect = (area instanceof Rectangle2D) ? (Rectangle2D) area : area.getBounds2D();
 1417  0
         if (isValid(rect)) {
 1418  0
             setVisibleArea(rect);
 1419  
         }
 1420  0
     }
 1421  
 
 1422  
     /**
 1423  
      * Returns the geometric shape to be used to delimitate an area. This shape is generally a
 1424  
      * rectangle but could also be an ellipse or another shape. The coordinates of the returned
 1425  
      * shape won't be taken into account. In fact, these coordinates will often be overwritten.
 1426  
      * The only things that matter are the class of the returned shape (e.g. {@link Ellipse2D}
 1427  
      * vs {@link Rectangle2D}) and any of its parameters not related to its position (e.g. arc
 1428  
      * size in a {@link RoundRectangle2D}).
 1429  
      * <p>
 1430  
      * The returned shape will generally be an instance of {@link RectangularShape}, but can also
 1431  
      * be an instance of {@link Line2D}. <strong>Any other class risks throwing a
 1432  
      * {@link ClassCastException} at execution</strong>.
 1433  
      * <p>
 1434  
      * The default implementation always returns a {@link Rectangle2D} object.
 1435  
      *
 1436  
      * @param  point Logical coordinates of the mouse at the moment the button is pressed. This
 1437  
      *         information can be used by subclasses that wish to consider the mouse position
 1438  
      *         before choosing a geometric shape.
 1439  
      * @return Shape as an instance of {@link RectangularShape} or {@link Line2D}, or {@code null}
 1440  
      *         to indicate that we do not want to select with the mouse.
 1441  
      */
 1442  
     protected Shape getMouseSelectionShape(final Point2D point) {
 1443  0
         return new Rectangle2D.Float();
 1444  
     }
 1445  
 
 1446  
     /**
 1447  
      * Indicates whether or not the magnifying glass is allowed to be
 1448  
      * displayed on this component.  By default, it is allowed.
 1449  
      *
 1450  
      * @return {@code true} if the magniying glass is allowed to be displayed.
 1451  
      */
 1452  
     public boolean isMagnifierEnabled() {
 1453  0
         return magnifierEnabled;
 1454  
     }
 1455  
 
 1456  
     /**
 1457  
      * Specifies whether or not the magnifying glass is allowed to be displayed on this component.
 1458  
      * Calling this method with the value {@code false} will hide the magnifying glass, delete the
 1459  
      * choice "Display magnifying glass" from the contextual menu and lead to all calls to
 1460  
      * <code>{@linkplain #setMagnifierVisible setMagnifierVisible}(true)</code> being ignored.
 1461  
      *
 1462  
      * @param enabled {@code true} if the magniying glass is allowed to be displayed.
 1463  
      */
 1464  
     public void setMagnifierEnabled(final boolean enabled) {
 1465  0
         magnifierEnabled = enabled;
 1466  0
         navigationPopupMenu = null;
 1467  0
         if (!enabled) {
 1468  0
             setMagnifierVisible(false);
 1469  
         }
 1470  0
     }
 1471  
 
 1472  
     /**
 1473  
      * Indicates whether or not the magnifying glass is visible.  By default, it is not visible.
 1474  
      * Call {@link #setMagnifierVisible(boolean)} to make it appear.
 1475  
      *
 1476  
      * @return {@code true} if the magniying glass is currently visible.
 1477  
      */
 1478  
     public boolean isMagnifierVisible() {
 1479  0
         return magnifier != null;
 1480  
     }
 1481  
 
 1482  
     /**
 1483  
      * Displays or hides the magnifying glass. If the magnifying glass is not visible and this
 1484  
      * method is called with the argument {@code true}, the magnifying glass will appear at the
 1485  
      * centre of the window.
 1486  
      *
 1487  
      * @param visible {@code true} for making the magniying glass visible.
 1488  
      */
 1489  
     public void setMagnifierVisible(final boolean visible) {
 1490  0
         setMagnifierVisible(visible, null);
 1491  0
     }
 1492  
 
 1493  
     /**
 1494  
      * Returns the color with which to tint magnifying glass.
 1495  
      *
 1496  
      * @return The current color of the magnifying glass interior.
 1497  
      */
 1498  
     public Paint getMagnifierGlass() {
 1499  0
         return magnifierGlass;
 1500  
     }
 1501  
 
 1502  
     /**
 1503  
      * Sets the color with which to tint magnifying glass.
 1504  
      *
 1505  
      * @param color The new color of the magnifying glass interior.
 1506  
      */
 1507  
     public void setMagnifierGlass(final Paint color) {
 1508  0
         final Paint old = magnifierGlass;
 1509  0
         magnifierGlass = color;
 1510  0
         firePropertyChange("magnifierGlass", old, color);
 1511  0
     }
 1512  
 
 1513  
     /**
 1514  
      * Returns the color of the magnifying glass's border.
 1515  
      *
 1516  
      * @return The current color of the magnifying glass border.
 1517  
      */
 1518  
     public Paint getMagnifierBorder() {
 1519  0
         return magnifierBorder;
 1520  
     }
 1521  
 
 1522  
     /**
 1523  
      * Sets the color of the magnifying glass's border.
 1524  
      *
 1525  
      * @param color The new color of the magnifying glass border.
 1526  
      */
 1527  
     public void setMagnifierBorder(final Paint color) {
 1528  0
         final Paint old = magnifierBorder;
 1529  0
         magnifierBorder = color;
 1530  0
         firePropertyChange("magnifierBorder", old, color);
 1531  0
     }
 1532  
 
 1533  
     /**
 1534  
      * Returns the scale factor that has been applied on the {@link Graphics2D} before invoking
 1535  
      * {@link #paintComponent(Graphics2D)}. This is always 1, except when painting the content
 1536  
      * of the magnifier glass.
 1537  
      */
 1538  
     final double getGraphicsScale() {
 1539  0
         return (flag == IS_PAINTING_MAGNIFIER) ? magnifierPower : 1;
 1540  
     }
 1541  
 
 1542  
     /**
 1543  
      * Corrects a pixel's coordinates for removing the effect of the magnifying glass. Without this
 1544  
      * method, transformations from pixels to geographic coordinates would not give accurate results
 1545  
      * for pixels inside the magnifying glass since the glass moves the pixel's apparent position.
 1546  
      * Invoking this method will remove deformation effects using the following steps:
 1547  
      * <p>
 1548  
      * <ul>
 1549  
      *   <li>If the pixel's coordinate {@code point} is outside the magnifying glass,
 1550  
      *       then this method do nothing.</li>
 1551  
      *   <li>Otherwise, if the pixel's coordinate is inside the magnifying glass, then this method
 1552  
      *       update {@code point} in such a way that it contains the position that the same pixel
 1553  
      *       would have in the absence of magnifying glass.</li>
 1554  
      * </ul>
 1555  
      *
 1556  
      * @param point In input, a pixel's coordinate as it appears on the screen. In output, the
 1557  
      *        coordinate that the same pixel would have if the magnifying glass wasn't presents.
 1558  
      */
 1559  
     @Override
 1560  
     public void correctApparentPixelPosition(final Point2D point) {
 1561  0
         if (magnifier != null && magnifier.contains(point)) {
 1562  0
             final double centerX = magnifier.getCenterX();
 1563  0
             final double centerY = magnifier.getCenterY();
 1564  
             /*
 1565  
              * The following code is equivalent to the following transformations, which
 1566  
              * must be identical to those which are applied in paintMagnifier(...).
 1567  
              *
 1568  
              *     translate(+centerX, +centerY);
 1569  
              *     scale    (magnifierPower, magnifierPower);
 1570  
              *     translate(-centerX, -centerY);
 1571  
              *     inverseTransform(point, point);
 1572  
              */
 1573  0
             point.setLocation((point.getX() - centerX) / magnifierPower + centerX,
 1574  
                               (point.getY() - centerY) / magnifierPower + centerY);
 1575  
         }
 1576  0
     }
 1577  
 
 1578  
     /**
 1579  
      * Displays or hides the magnifying glass. If the magnifying glass isn't visible and this
 1580  
      * method is called with the argument {@code true}, the magnifying glass will be displayed
 1581  
      * centred on the specified coordinate.
 1582  
      *
 1583  
      * @param visible {@code true} to display the magnifying glass or {@code false} to hide it.
 1584  
      * @param center  Central coordinate on which to display the magnifying glass.  If the
 1585  
      *        magnifying glass was initially invisible, it will appear centred on this coordinate
 1586  
      *        (or in the centre of the screen if {@code center} is null). If the magnifying glass
 1587  
      *        was already visible and {@code center} is not null, it will be moved to centre it on
 1588  
      *        the specified coordinate.
 1589  
      */
 1590  
     private void setMagnifierVisible(final boolean visible, final Point center) {
 1591  0
         MouseReshapeTracker magnifier = this.magnifier;
 1592  0
         if (visible && magnifierEnabled) {
 1593  0
             if (magnifier == null) {
 1594  0
                 Rectangle bounds = getZoomableBounds(); // Do not modify the Rectangle!
 1595  0
                 if (bounds.isEmpty()) bounds = new Rectangle(0, 0, DEFAULT_SIZE, DEFAULT_SIZE);
 1596  0
                 final int size = Math.min(Math.min(bounds.width, bounds.height), DEFAULT_MAGNIFIER_SIZE);
 1597  
                 final int x, y;
 1598  0
                 if (center != null) {
 1599  0
                     x = center.x - size / 2;
 1600  0
                     y = center.y - size / 2;
 1601  
                 } else {
 1602  0
                     x = bounds.x + (bounds.width - size) / 2;
 1603  0
                     y = bounds.y + (bounds.height - size) / 2;
 1604  
                 }
 1605  0
                 this.magnifier = magnifier = new MouseReshapeTracker(new RoundRectangle2D.Float(x, y, size, size, 24, 24)) {
 1606  0
                     @Override protected void stateWillChange(final boolean isAdjusting) {repaintMagnifier();}
 1607  0
                     @Override protected void stateChanged   (final boolean isAdjusting) {repaintMagnifier();}
 1608  
                 };
 1609  0
                 magnifier.setClip(bounds);
 1610  0
                 magnifier.setAdjustable(SwingConstants.NORTH, true);
 1611  0
                 magnifier.setAdjustable(SwingConstants.SOUTH, true);
 1612  0
                 magnifier.setAdjustable(SwingConstants.EAST , true);
 1613  0
                 magnifier.setAdjustable(SwingConstants.WEST , true);
 1614  
 
 1615  0
                 addMouseListener      (magnifier);
 1616  0
                 addMouseMotionListener(magnifier);
 1617  0
                 firePropertyChange("magnifierVisible", Boolean.FALSE, Boolean.TRUE);
 1618  0
                 repaintMagnifier();
 1619  0
             } else if (center != null) {
 1620  0
                 final Rectangle2D frame = magnifier.getFrame();
 1621  0
                 final double width  = frame.getWidth();
 1622  0
                 final double height = frame.getHeight();
 1623  0
                 magnifier.setFrame(center.x - 0.5 * width,
 1624  
                                    center.y - 0.5 * height, width, height);
 1625  0
             }
 1626  0
         } else if (magnifier != null) {
 1627  0
             repaintMagnifier();
 1628  0
             removeMouseMotionListener(magnifier);
 1629  0
             removeMouseListener      (magnifier);
 1630  0
             setCursor(null);
 1631  0
             this.magnifier = null;
 1632  0
             firePropertyChange("magnifierVisible", Boolean.TRUE, Boolean.FALSE);
 1633  
         }
 1634  0
     }
 1635  
 
 1636  
     /**
 1637  
      * Adds navigation options to the specified menu. Menus such as "Zoom in" and "Zoom out" will
 1638  
      * be automatically added to the menu together with the appropriate short-cut keys.
 1639  
      *
 1640  
      * @param menu The menu in which to add navigation options.
 1641  
      */
 1642  
     public void buildNavigationMenu(final JMenu menu) {
 1643  0
         buildNavigationMenu(menu, null);
 1644  0
     }
 1645  
 
 1646  
     /**
 1647  
      * Adds navigation options to the specified menu. Menus such as "Zoom in" and "Zoom out" will
 1648  
      * be automatically added to the menu together with the appropriate short-cut keys.
 1649  
      */
 1650  
     private void buildNavigationMenu(final JMenu menu, final JPopupMenu popup) {
 1651  0
         int groupIndex = 0;
 1652  0
         boolean firstMenu = true;
 1653  0
         final ActionMap actionMap = getActionMap();
 1654  0
         for (int i=0; i<ACTION_ID.length; i++) {
 1655  0
             final Action action = actionMap.get(ACTION_ID[i]);
 1656  0
             if (action!=null && action.getValue(Action.NAME)!=null) {
 1657  
                 /*
 1658  
                  * Checks whether the next item belongs to a new group.
 1659  
                  * If this is the case, it will be necessary to add a separator
 1660  
                  * before the next menu.
 1661  
                  */
 1662  0
                 final int lastGroupIndex = groupIndex;
 1663  0
                 while ((ACTION_TYPE[i] & GROUP[groupIndex]) == 0) {
 1664  0
                     groupIndex = (groupIndex+1) % GROUP.length;
 1665  0
                     if (groupIndex == lastGroupIndex) {
 1666  0
                         break;
 1667  
                     }
 1668  
                 }
 1669  
                 /*
 1670  
                  * Adds an item to the menu.
 1671  
                  */
 1672  0
                 if (menu != null) {
 1673  0
                     if (groupIndex!=lastGroupIndex && !firstMenu) {
 1674  0
                         menu.addSeparator();
 1675  
                     }
 1676  0
                     final JMenuItem item = new JMenuItem(action);
 1677  0
                     item.setAccelerator((KeyStroke) action.getValue(Action.ACCELERATOR_KEY));
 1678  0
                     menu.add(item);
 1679  
                 }
 1680  0
                 if (popup != null) {
 1681  0
                     if (groupIndex!=lastGroupIndex && !firstMenu) {
 1682  0
                         popup.addSeparator();
 1683  
                     }
 1684  0
                     final JMenuItem item = new JMenuItem(action);
 1685  0
                     item.setAccelerator((KeyStroke) action.getValue(Action.ACCELERATOR_KEY));
 1686  0
                     popup.add(item);
 1687  
                 }
 1688  0
                 firstMenu = false;
 1689  
             }
 1690  
         }
 1691  0
     }
 1692  
 
 1693  
     /**
 1694  
      * Menu with a position.  This class retains the exact coordinates of the
 1695  
      * place the user clicked when this menu was invoked.
 1696  
      *
 1697  
      * @author Martin Desruisseaux (IRD)
 1698  
      * @version 3.00
 1699  
      *
 1700  
      * @since 2.0
 1701  
      * @module
 1702  
      */
 1703  
     @SuppressWarnings("serial")
 1704  
     private static final class PointPopupMenu extends JPopupMenu {
 1705  
         /**
 1706  
          * Coordinates of the point the user clicked on.
 1707  
          */
 1708  
         public final Point point;
 1709  
 
 1710  
         /**
 1711  
          * Constructs a menu, retaining the specified coordinate.
 1712  
          */
 1713  0
         public PointPopupMenu(final Point point) {
 1714  0
             this.point = point;
 1715  0
         }
 1716  
     }
 1717  
 
 1718  
     /**
 1719  
      * Method called automatically when the user clicks on the right mouse button.  The default
 1720  
      * implementation displays a contextual menu containing navigation options.
 1721  
      *
 1722  
      * @param  event Mouse event. This object contains the mouse coordinates
 1723  
      *         in geographic coordinates (as well as pixel coordinates).
 1724  
      * @return The contextual menu, or {@code null} to avoid displaying the menu.
 1725  
      */
 1726  
     protected JPopupMenu getPopupMenu(final MouseEvent event) {
 1727  0
         if (getZoomableBounds().contains(event.getX(), event.getY())) {
 1728  0
             if (navigationPopupMenu == null) {
 1729  0
                 navigationPopupMenu = new PointPopupMenu(event.getPoint());
 1730  0
                 if (magnifierEnabled) {
 1731  0
                     final Vocabulary resources = Vocabulary.getResources(getLocale());
 1732  0
                     final JMenuItem item = new JMenuItem(
 1733  
                             resources.getString(Vocabulary.Keys.SHOW_MAGNIFIER));
 1734  0
                     item.addActionListener(new ActionListener() {
 1735  
                         @Override public void actionPerformed(final ActionEvent event) {
 1736  0
                             setMagnifierVisible(true, navigationPopupMenu.point);
 1737  0
                         }
 1738  
                     });
 1739  0
                     navigationPopupMenu.add(item);
 1740  0
                     navigationPopupMenu.addSeparator();
 1741  
                 }
 1742  0
                 buildNavigationMenu(null, navigationPopupMenu);
 1743  
             } else {
 1744  0
                 navigationPopupMenu.point.x = event.getX();
 1745  0
                 navigationPopupMenu.point.y = event.getY();
 1746  
             }
 1747  0
             return navigationPopupMenu;
 1748  
         } else {
 1749  0
             return null;
 1750  
         }
 1751  
     }
 1752  
 
 1753  
     /**
 1754  
      * Method called automatically when the user clicks on the right mouse
 1755  
      * button inside the magnifying glass. The default implementation displays
 1756  
      * a contextual menu which contains magnifying glass options.
 1757  
      *
 1758  
      * @param  event Mouse event containing amongst others, the mouse position.
 1759  
      * @return The contextual menu, or {@code null} to avoid displaying the menu.
 1760  
      */
 1761  
     protected JPopupMenu getMagnifierMenu(final MouseEvent event) {
 1762  0
         final Vocabulary resources = Vocabulary.getResources(getLocale());
 1763  0
         final JPopupMenu menu = new JPopupMenu(resources.getString(Vocabulary.Keys.MAGNIFIER));
 1764  0
         final JMenuItem  item = new JMenuItem (resources.getString(Vocabulary.Keys.HIDE));
 1765  0
         item.addActionListener(new ActionListener() {
 1766  
             @Override public void actionPerformed(final ActionEvent event) {
 1767  0
                 setMagnifierVisible(false);
 1768  0
             }
 1769  
         });
 1770  0
         menu.add(item);
 1771  0
         return menu;
 1772  
     }
 1773  
 
 1774  
     /**
 1775  
      * Displays the navigation contextual menu, provided the mouse event is
 1776  
      * in fact the one which normally displays this menu.
 1777  
      */
 1778  
     private void mayShowPopupMenu(final MouseEvent event) {
 1779  0
         if (event.getID() == MouseEvent.MOUSE_PRESSED &&
 1780  
                 (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0)
 1781  
         {
 1782  0
             requestFocus();
 1783  
         }
 1784  0
         if (event.isPopupTrigger()) {
 1785  0
             final Point point      = event.getPoint();
 1786  0
             final JPopupMenu popup = (magnifier != null && magnifier.contains(point)) ?
 1787  
                     getMagnifierMenu(event) : getPopupMenu(event);
 1788  0
             if (popup != null) {
 1789  0
                 final Component source  = event.getComponent();
 1790  0
                 final Window    window  = SwingUtilities.getWindowAncestor(source);
 1791  0
                 if (window != null) {
 1792  0
                     final Toolkit   toolkit = source.getToolkit();
 1793  0
                     final Insets    insets  = toolkit.getScreenInsets(window.getGraphicsConfiguration());
 1794  0
                     final Dimension screen  = toolkit.getScreenSize();
 1795  0
                     final Dimension size    = popup.getPreferredSize();
 1796  0
                     SwingUtilities.convertPointToScreen(point, source);
 1797  0
                     screen.width  -= (size.width  + insets.right);
 1798  0
                     screen.height -= (size.height + insets.bottom);
 1799  0
                     if (point.x > screen.width) {
 1800  0
                         point.x = screen.width;
 1801  
                     }
 1802  0
                     if (point.y > screen.height) {
 1803  0
                         point.y = screen.height;
 1804  
                     }
 1805  0
                     if (point.x < insets.left) {
 1806  0
                         point.x = insets.left;
 1807  
                     }
 1808  0
                     if (point.y < insets.top) {
 1809  0
                         point.y = insets.top;
 1810  
                     }
 1811  0
                     SwingUtilities.convertPointFromScreen(point, source);
 1812  0
                     popup.show(source, point.x, point.y);
 1813  
                 }
 1814  
             }
 1815  
         }
 1816  0
     }
 1817  
 
 1818  
     /**
 1819  
      * Method called automatically when user moves the mouse wheel. This method
 1820  
      * performs a zoom centered on the mouse position.
 1821  
      */
 1822  
     private void mouseWheelMoved(final MouseWheelEvent event) {
 1823  0
         if (event.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
 1824  0
             int rotation  = event.getUnitsToScroll();
 1825  0
             double scale  = 1 + (AMOUNT_SCALE - 1) * Math.abs(rotation);
 1826  0
             Point2D point = new Point2D.Double(event.getX(), event.getY());
 1827  0
             if (rotation > 0) {
 1828  0
                 scale = 1 / scale;
 1829  
             }
 1830  0
             if (magnifier != null && magnifier.contains(point)) {
 1831  0
                 magnifierPower *= scale;
 1832  0
                 repaintMagnifier();
 1833  
             } else {
 1834  0
                 correctApparentPixelPosition(point);
 1835  0
                 transform(UNIFORM_SCALE & allowedActions, scale, point);
 1836  
             }
 1837  0
             event.consume();
 1838  
         }
 1839  0
     }
 1840  
 
 1841  
     /**
 1842  
      * Method called each time the size or the position of the component changes.
 1843  
      * The {@link #repaint} method is not called because there is already a repaint command in
 1844  
      * the queue. The {@link #transform} method is not called neither because the zoom hasn't
 1845  
      * really changed; we have simply discovered a part of the window which was hidden before.
 1846  
      * However, we still need to adjust the scrollbars.
 1847  
      */
 1848  
     private void processSizeEvent(final ComponentEvent event) {
 1849  2
         if (zoomIsReset || !isValid(visibleArea)) {
 1850  2
             disableRepaint = true;
 1851  
             try {
 1852  2
                 reset();
 1853  
             } finally {
 1854  2
                 disableRepaint = false;
 1855  2
             }
 1856  
         }
 1857  2
         if (magnifier != null) {
 1858  0
             magnifier.setClip(getZoomableBounds());
 1859  
         }
 1860  2
         final Object[] listeners = listenerList.getListenerList();
 1861  2
         for (int i = listeners.length; (i-=2) >= 0;) {
 1862  0
             if (listeners[i] == ZoomChangeListener.class) {
 1863  0
                 if (listeners[i + 1] instanceof Synchronizer) try {
 1864  0
                     ((ZoomChangeListener) listeners[i + 1]).zoomChanged(null);
 1865  0
                 } catch (RuntimeException exception) {
 1866  0
                     unexpectedException("processSizeEvent", exception);
 1867  0
                 }
 1868  
             }
 1869  
         }
 1870  2
     }
 1871  
 
 1872  
     /**
 1873  
      * Returns an object which displays this {@code ZoomPane} with the scrollbars.
 1874  
      *
 1875  
      * @return A swing component displaying this {@code ZoomPane} together with scrollbars.
 1876  
      */
 1877  
     public JComponent createScrollPane() {
 1878  3
         return new ScrollPane();
 1879  
     }
 1880  
 
 1881  
     /**
 1882  
      * Convenience method which fetches a scrollbar model. Should actually be declared inside
 1883  
      * {@link ScrollPane}, but we are not allowed to declare static methods in non-static inner
 1884  
      * classes.
 1885  
      */
 1886  
     static BoundedRangeModel getModel(final JScrollBar bar) {
 1887  0
         return (bar != null) ? bar.getModel() : null;
 1888  
     }
 1889  
 
 1890  
     /**
 1891  
      * The scroll panel for {@link ZoomPane}. The standard {@link javax.swing.JScrollPane}
 1892  
      * class is not used because it is difficult to get {@link javax.swing.JViewport} to
 1893  
      * cooperate with transformations already handled by {@link ZoomPane#zoom}.
 1894  
      *
 1895  
      * @author Martin Desruisseaux (IRD)
 1896  
      * @version 3.00
 1897  
      *
 1898  
      * @since 2.0
 1899  
      * @module
 1900  
      */
 1901  
     @SuppressWarnings("serial")
 1902  
     private final class ScrollPane extends JComponent implements PropertyChangeListener {
 1903  
         /**
 1904  
          * The horizontal scrollbar, or {@code null} if none.
 1905  
          */
 1906  
         private final JScrollBar scrollbarX;
 1907  
 
 1908  
         /**
 1909  
          * The vertical scrollbar, or {@code null} if none.
 1910  
          */
 1911  
         private final JScrollBar scrollbarY;
 1912  
 
 1913  
         /**
 1914  
          * Constructs a scroll pane for the enclosing {@link ZoomPane}.
 1915  
          */
 1916  3
         public ScrollPane() {
 1917  3
             setOpaque(false);
 1918  3
             setLayout(new GridBagLayout());
 1919  
             /*
 1920  
              * Sets up the scrollbars.
 1921  
              */
 1922  3
             if ((allowedActions & TRANSLATE_X) != 0) {
 1923  3
                 scrollbarX = new JScrollBar(JScrollBar.HORIZONTAL);
 1924  3
                 scrollbarX.setUnitIncrement ((int) (AMOUNT_TRANSLATE));
 1925  3
                 scrollbarX.setBlockIncrement((int) (AMOUNT_TRANSLATE * ENHANCEMENT_FACTOR));
 1926  
             } else {
 1927  0
                 scrollbarX  = null;
 1928  
             }
 1929  3
             if ((allowedActions & TRANSLATE_Y) != 0) {
 1930  2
                 scrollbarY = new JScrollBar(JScrollBar.VERTICAL);
 1931  2
                 scrollbarY.setUnitIncrement ((int) (AMOUNT_TRANSLATE));
 1932  2
                 scrollbarY.setBlockIncrement((int) (AMOUNT_TRANSLATE * ENHANCEMENT_FACTOR));
 1933  
             } else {
 1934  1
                 scrollbarY  = null;
 1935  
             }
 1936  
             /*
 1937  
              * Adds the scrollbars in the scroll pane.
 1938  
              */
 1939  3
             final GridBagConstraints c = new GridBagConstraints();
 1940  3
             if (scrollbarX != null) {
 1941  3
                 c.gridx = 0; c.weightx = 1;
 1942  3
                 c.gridy = 1; c.weighty = 0;
 1943  3
                 c.fill = HORIZONTAL;
 1944  3
                 add(scrollbarX, c);
 1945  
             }
 1946  3
             if (scrollbarY != null) {
 1947  2
                 c.gridx = 1; c.weightx = 0;
 1948  2
                 c.gridy = 0; c.weighty = 1;
 1949  2
                 c.fill = VERTICAL;
 1950  2
                 add(scrollbarY, c);
 1951  
             }
 1952  3
             if (scrollbarX != null && scrollbarY != null) {
 1953  2
                 final JComponent corner = new JPanel(false);
 1954  2
                 c.gridx = 1; c.weightx = 0;
 1955  2
                 c.gridy = 1; c.weighty = 0;
 1956  2
                 c.fill = BOTH;
 1957  2
                 add(corner, c);
 1958  
             }
 1959  3
             c.fill = BOTH;
 1960  3
             c.gridx = 0; c.weightx = 1;
 1961  3
             c.gridy = 0; c.weighty = 1;
 1962  3
             add(ZoomPane.this, c);
 1963  3
         }
 1964  
 
 1965  
         /**
 1966  
          * Invoked when this {@code ScrollPane} is added in a {@link Container}.
 1967  
          * This method registers all required listeners.
 1968  
          */
 1969  
         @Override
 1970  
         public void addNotify() {
 1971  0
             super.addNotify();
 1972  0
             tieModels(getModel(scrollbarX), getModel(scrollbarY));
 1973  0
             ZoomPane.this.addPropertyChangeListener("zoom.insets", this);
 1974  0
         }
 1975  
 
 1976  
         /**
 1977  
          * Invoked when this {@code ScrollPane} is removed from a {@link Container}.
 1978  
          * This method unregisters all listeners.
 1979  
          */
 1980  
         @Override
 1981  
         public void removeNotify() {
 1982  0
             ZoomPane.this.removePropertyChangeListener("zoom.insets", this);
 1983  0
             untieModels(getModel(scrollbarX), getModel(scrollbarY));
 1984  0
             super.removeNotify();
 1985  0
         }
 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  
         @Override
 1992  
         public void propertyChange(final PropertyChangeEvent event) {
 1993  0
             final Insets old    = (Insets) event.getOldValue();
 1994  0
             final Insets insets = (Insets) event.getNewValue();
 1995  0
             final GridBagLayout layout = (GridBagLayout) getLayout();
 1996  0
             if (scrollbarX != null && (old.left != insets.left || old.right != insets.right)) {
 1997  0
                 final GridBagConstraints c = layout.getConstraints(scrollbarX);
 1998  0
                 c.insets.left  = insets.left;
 1999  0
                 c.insets.right = insets.right;
 2000  0
                 layout.setConstraints(scrollbarX, c);
 2001  0
                 scrollbarX.invalidate();
 2002  
             }
 2003  0
             if (scrollbarY != null && (old.top != insets.top || old.bottom != insets.bottom)) {
 2004  0
                 final GridBagConstraints c = layout.getConstraints(scrollbarY);
 2005  0
                 c.insets.top    = insets.top;
 2006  0
                 c.insets.bottom = insets.bottom;
 2007  0
                 layout.setConstraints(scrollbarY, c);
 2008  0
                 scrollbarY.invalidate();
 2009  
             }
 2010  0
         }
 2011  
     }
 2012  
 
 2013  
     /**
 2014  
      * Synchronises the position and the range of the models <var>x</var> and <var>y</var> with the
 2015  
      * position of the zoom. The models <var>x</var> and <var>y</var> are generally associated with
 2016  
      * horizontal and vertical scrollbars.  When the position of a scrollbar is adjusted, the zoom
 2017  
      * is consequently adjusted. Inversely, when the zoom is modified, the positions and ranges of
 2018  
      * the scrollbars are consequently adjusted.
 2019  
      *
 2020  
      * @param x Model of the horizontal scrollbar or {@code null} if there isn't one.
 2021  
      * @param y Model of the vertical scrollbar or {@code null} if there isn't one.
 2022  
      */
 2023  
     public void tieModels(final BoundedRangeModel x, final BoundedRangeModel y) {
 2024  0
         if (x != null || y != null) {
 2025  0
             final Synchronizer listener = new Synchronizer(x, y);
 2026  0
             addZoomChangeListener(listener);
 2027  0
             if (x != null) x.addChangeListener(listener);
 2028  0
             if (y != null) y.addChangeListener(listener);
 2029  
         }
 2030  0
     }
 2031  
 
 2032  
     /**
 2033  
      * Cancels the synchronization between the specified <var>x</var> and <var>y</var> models
 2034  
      * and the zoom of this {@code ZoomPane} object. The {@link ChangeListener} and
 2035  
      * {@link ZoomChangeListener} objects that were created are deleted.
 2036  
      *
 2037  
      * @param x Model of the horizontal scrollbar or {@code null} if there isn't one.
 2038  
      * @param y Model of the vertical scrollbar or {@code null} if there isn't one.
 2039  
      */
 2040  
     public void untieModels(final BoundedRangeModel x, final BoundedRangeModel y) {
 2041  0
         final EventListener[] listeners = getListeners(ZoomChangeListener.class);
 2042  0
         for (int i = 0; i < listeners.length; i++) {
 2043  0
             if (listeners[i] instanceof Synchronizer) {
 2044  0
                 final Synchronizer s = (Synchronizer) listeners[i];
 2045  0
                 if (s.xm == x && s.ym == y) {
 2046  0
                     removeZoomChangeListener(s);
 2047  0
                     if (x != null) x.removeChangeListener(s);
 2048  0
                     if (y != null) y.removeChangeListener(s);
 2049  
                 }
 2050  
             }
 2051  
         }
 2052  0
     }
 2053  
 
 2054  
     /**
 2055  
      * Object responsible for synchronizing a {@link JScrollPane} object with scrollbars.
 2056  
      * Whilst not generally useful, it would be possible to synchronize several pairs of
 2057  
      * {@link BoundedRangeModel} objects on one {@code ZoomPane} object.
 2058  
      *
 2059  
      * @author Martin Desruisseaux (IRD)
 2060  
      * @version 3.00
 2061  
      *
 2062  
      * @since 2.0
 2063  
      * @module
 2064  
      */
 2065  
     private final class Synchronizer implements ChangeListener, ZoomChangeListener {
 2066  
         /**
 2067  
          * Model to synchronize with {@link ZoomPane}.
 2068  
          */
 2069  
         public final BoundedRangeModel xm, ym;
 2070  
 
 2071  
         /**
 2072  
          * Indicates whether the scrollbars are being adjusted in response to {@link #zoomChanged}.
 2073  
          * If this is the case, {@link #stateChanged} must not make any other adjustments.
 2074  
          */
 2075  
         private transient boolean isAdjusting;
 2076  
 
 2077  
         /**
 2078  
          * Cached {@code ZoomPane} bounds. Used in order to avoid too many object allocations
 2079  
          * on the heap.
 2080  
          */
 2081  
         private transient Rectangle bounds;
 2082  
 
 2083  
         /**
 2084  
          * Constructs an object which synchronises a pair of {@link BoundedRangeModel} with
 2085  
          * {@link ZoomPane}.
 2086  
          */
 2087  0
         public Synchronizer(final BoundedRangeModel xm, final BoundedRangeModel ym) {
 2088  0
             this.xm = xm;
 2089  0
             this.ym = ym;
 2090  0
         }
 2091  
 
 2092  
         /**
 2093  
          * Method called automatically each time the position of one of the scrollbars changes.
 2094  
          */
 2095  
         @Override
 2096  
         public void stateChanged(final ChangeEvent event) {
 2097  0
             if (!isAdjusting) {
 2098  0
                 final boolean valueIsAdjusting = ((BoundedRangeModel) event.getSource()).getValueIsAdjusting();
 2099  0
                 if (paintingWhileAdjusting || !valueIsAdjusting) {
 2100  
                     /*
 2101  
                      * Scroll view coordinates are computed using the following steps:
 2102  
                      *
 2103  
                      *   1) Get the logical coordinates for the whole area.
 2104  
                      *   2) Transform to pixel space using current zoom.
 2105  
                      *   3) Clip to the scrollbar's position (in pixels).
 2106  
                      *   4) Transform back to the logical space.
 2107  
                      *   5) Set the visible area to the resulting rectangle.
 2108  
                      */
 2109  0
                     Rectangle2D area = getArea();
 2110  0
                     if (isValid(area)) {
 2111  0
                         area = XAffineTransform.transform(zoom, area, null);
 2112  0
                         double x = area.getX();
 2113  0
                         double y = area.getY();
 2114  
                         double width, height;
 2115  0
                         if (xm != null) {
 2116  0
                             x    += xm.getValue();
 2117  0
                             width = xm.getExtent();
 2118  
                         } else {
 2119  0
                             width = area.getWidth();
 2120  
                         }
 2121  0
                         if (ym != null) {
 2122  0
                             y     += ym.getValue();
 2123  0
                             height = ym.getExtent();
 2124  
                         } else {
 2125  0
                             height = area.getHeight();
 2126  
                         }
 2127  0
                         area.setRect(x, y, width, height);
 2128  0
                         bounds = getBounds(bounds);
 2129  
                         try {
 2130  0
                             area = XAffineTransform.inverseTransform(zoom, area, area);
 2131  
                             try {
 2132  0
                                 isAdjusting = true;
 2133  0
                                 transform(setVisibleArea(area, bounds=getBounds(bounds), 0));
 2134  
                             } finally {
 2135  0
                                 isAdjusting = false;
 2136  0
                             }
 2137  0
                         } catch (NoninvertibleTransformException exception) {
 2138  0
                             unexpectedException("stateChanged", exception);
 2139  0
                         }
 2140  
                     }
 2141  
                 }
 2142  0
                 if (!valueIsAdjusting) {
 2143  0
                     zoomChanged(null);
 2144  
                 }
 2145  
             }
 2146  0
         }
 2147  
 
 2148  
         /**
 2149  
          * Method called each time the zoom changes.
 2150  
          *
 2151  
          * @param change Ignored. Can be null and will effectively sometimes be null.
 2152  
          */
 2153  
         @Override
 2154  
         public void zoomChanged(final ZoomChangeEvent change) {
 2155  0
             if (!isAdjusting) {
 2156  0
                 Rectangle2D area = getArea();
 2157  0
                 if (isValid(area)) {
 2158  0
                     area = XAffineTransform.transform(zoom, area, null);
 2159  
                     try {
 2160  0
                         isAdjusting = true;
 2161  0
                         setRangeProperties(xm, area.getX(), getWidth(),  area.getWidth());
 2162  0
                         setRangeProperties(ym, area.getY(), getHeight(), area.getHeight());
 2163  
                     }
 2164  
                     finally {
 2165  0
                         isAdjusting = false;
 2166  0
                     }
 2167  
                 }
 2168  
             }
 2169  0
         }
 2170  
     }
 2171  
 
 2172  
     /**
 2173  
      * Adjusts the values of a model. The minimums and maximums are adjusted as needed in order to
 2174  
      * include the value and its range. This adjustment is necessary in order to avoid chaotic
 2175  
      * behaviour when the user drags the slider whilst a part of the graphic is outside the zone
 2176  
      * initially planned for {@link #getArea}.
 2177  
      */
 2178  
     private static void setRangeProperties(final BoundedRangeModel model,
 2179  
             final double value, final int extent, final double max)
 2180  
     {
 2181  0
         if (model != null) {
 2182  0
             final int pos = (int) Math.round(-value);
 2183  0
             model.setRangeProperties(pos, extent, Math.min(0, pos),
 2184  
                     Math.max((int) Math.round(max), pos + extent), false);
 2185  
         }
 2186  0
     }
 2187  
 
 2188  
     /**
 2189  
      * Modifies the position in pixels of the visible part of {@code ZoomPane}. {@code viewSize}
 2190  
      * is the size {@code ZoomPane} would be (in pixels) if its visible surface covered the whole
 2191  
      * of the {@link #getArea} region with the current zoom (Note: {@code viewSize} can be obtained
 2192  
      * by {@link #getPreferredSize} if {@link #setPreferredSize} hasn't been called with a non-null
 2193  
      * value). Therefore, by definition, the region {@link #getArea} converted into pixel space would
 2194  
      * give the rectangle <code>bounds = Rectangle(0, 0, viewSize.width, viewSize.height)</code>.
 2195  
      * <p>
 2196  
      * This {@code scrollRectToVisible} method allows us to define the sub-region of {@code bounds}
 2197  
      * which must appear in the {@code ZoomPane} window.
 2198  
      *
 2199  
      * @param rect The region to be made visible.
 2200  
      */
 2201  
     @Override
 2202  
     public void scrollRectToVisible(final Rectangle rect) {
 2203  0
         Rectangle2D area = getArea();
 2204  0
         if (isValid(area)) {
 2205  0
             area = XAffineTransform.transform(zoom, area, null);
 2206  0
             area.setRect(area.getX() + rect.getX(), area.getY() + rect.getY(),
 2207  
                          rect.getWidth(), rect.getHeight());
 2208  
             try {
 2209  0
                 setVisibleArea(XAffineTransform.inverseTransform(zoom, area, area));
 2210  0
             } catch (NoninvertibleTransformException exception) {
 2211  0
                 unexpectedException("scrollRectToVisible", exception);
 2212  0
             }
 2213  
         }
 2214  0
     }
 2215  
 
 2216  
     /**
 2217  
      * Indicates whether or not this {@code ZoomPane} object should be repainted when the user
 2218  
      * moves the scrollbar slider. The scrollbars (or other models) involved are those which have
 2219  
      * been synchronised with this {@code ZoomPane} object through the {@link #tieModels} method.
 2220  
      * The default value is {@code false}, which means that {@code ZoomPane} will wait until the
 2221  
      * user releases the slider before repainting.
 2222  
      *
 2223  
      * @return {@code true} if the zoom pane is painted while the user is scrolling.
 2224  
      */
 2225  
     public boolean isPaintingWhileAdjusting() {
 2226  0
         return paintingWhileAdjusting;
 2227  
     }
 2228  
 
 2229  
     /**
 2230  
      * Defines whether or not this {@code ZoomPane} object should repaint the map when the user
 2231  
      * moves the scrollbar slider. A fast computer is recommended if this flag is to be set to
 2232  
      * {@code true}.
 2233  
      *
 2234  
      * @param flag {@code true} if the zoom pane should be painted while the user is scrolling.
 2235  
      */
 2236  
     public void setPaintingWhileAdjusting(final boolean flag) {
 2237  3
         paintingWhileAdjusting = flag;
 2238  3
     }
 2239  
 
 2240  
     /**
 2241  
      * Declares that a part of this pane needs to be repainted. This method simply redefines the
 2242  
      * method of the parent class in order to take into account a case where the magnifying glass
 2243  
      * is displayed.
 2244  
      */
 2245  
     @Override
 2246  
     public void repaint(final long tm, final int x, final int y, final int width, final int height) {
 2247  15
         super.repaint(tm, x, y, width, height);
 2248  15
         if (magnifier != null && magnifier.intersects(x, y, width, height)) {
 2249  
             // If the part to paint is inside the magnifying glass,
 2250  
             // the fact that the magnifying glass is zooming in means
 2251  
             // we have to repaint a little more than that which was requested.
 2252  0
             repaintMagnifier();
 2253  
         }
 2254  15
     }
 2255  
 
 2256  
     /**
 2257  
      * Declares that the magnifying glass needs to be repainted. A {@link #repaint()} command is
 2258  
      * sent with the bounds of the magnifying glass as coordinates (taking into account its outline).
 2259  
      */
 2260  
     private void repaintMagnifier() {
 2261  0
         final Rectangle bounds = magnifier.getBounds();
 2262  0
         bounds.x      -= 4;
 2263  0
         bounds.y      -= 4;
 2264  0
         bounds.width  += 8;
 2265  0
         bounds.height += 8;
 2266  0
         super.repaint(0, bounds.x, bounds.y, bounds.width, bounds.height);
 2267  0
     }
 2268  
 
 2269  
     /**
 2270  
      * Paints the magnifying glass. This method is invoked after
 2271  
      * {@link #paintComponent(Graphics2D)} if a magnifying glass is visible.
 2272  
      *
 2273  
      * @param graphics The graphics where to paint the magnifying glass.
 2274  
      */
 2275  
     protected void paintMagnifier(final Graphics2D graphics) {
 2276  0
         final double centerX = magnifier.getCenterX();
 2277  0
         final double centerY = magnifier.getCenterY();
 2278  0
         final Stroke  stroke =  graphics.getStroke();
 2279  0
         final Paint    paint =  graphics.getPaint();
 2280  0
         graphics.setStroke(new BasicStroke(6));
 2281  0
         graphics.setPaint (magnifierBorder);
 2282  0
         graphics.draw     (magnifier);
 2283  0
         graphics.setStroke(stroke);
 2284  0
         graphics.clip     (magnifier); // Coordinates in pixels!
 2285  0
         graphics.setPaint (magnifierGlass);
 2286  0
         graphics.fill     (magnifier.getBounds2D());
 2287  0
         graphics.setPaint (paint);
 2288  0
         graphics.translate(+centerX, +centerY);
 2289  0
         graphics.scale    (magnifierPower, magnifierPower);
 2290  0
         graphics.translate(-centerX, -centerY);
 2291  
         // Note: the transformations performed here must be identical to those
 2292  
         //       performed in pixelToLogical(...).
 2293  0
         paintComponent(graphics);
 2294  0
     }
 2295  
 
 2296  
     /**
 2297  
      * Paints this component. Subclass must override this method in order to draw the
 2298  
      * {@code ZoomPane} content. For most implementations, the first line in this method
 2299  
      * will be <code>graphics.transform({@linkplain #zoom})</code>.
 2300  
      *
 2301  
      * @param graphics The graphics where to paint this component.
 2302  
      */
 2303  
     protected abstract void paintComponent(final Graphics2D graphics);
 2304  
 
 2305  
     /**
 2306  
      * Prints this component. The default implementation invokes
 2307  
      * {@link #paintComponent(Graphics2D)}.
 2308  
      *
 2309  
      * @param graphics The graphics where to print this component.
 2310  
      */
 2311  
     protected void printComponent(final Graphics2D graphics) {
 2312  2
         paintComponent(graphics);
 2313  2
     }
 2314  
 
 2315  
     /**
 2316  
      * Paints this component. This method is declared final in order to avoid unintentional
 2317  
      * overriding. Override {@link #paintComponent(Graphics2D)} instead.
 2318  
      *
 2319  
      * @param graphics The graphics where to paint this component.
 2320  
      */
 2321  
     @Override
 2322  
     protected final void paintComponent(final Graphics graphics) {
 2323  0
         flag = IS_PAINTING;
 2324  0
         super.paintComponent(graphics);
 2325  
         /*
 2326  
          * The JComponent.paintComponent(...) method creates a temporary Graphics2D object,
 2327  
          * then calls ComponentUI.update(...) with this graphic as a parameter. This method
 2328  
          * clears the screen background then calls ComponentUI.paint(...). This last method
 2329  
          * has been redefined further up (our {@link #UI}) object in such a way that it calls
 2330  
          * itself paintComponent(Graphics2D). A complicated path, but we don't have much
 2331  
          * choice and it is, after all, quite efficient.
 2332  
          */
 2333  0
         if (magnifier != null) {
 2334  0
             flag = IS_PAINTING_MAGNIFIER;
 2335  0
             super.paintComponent(graphics);
 2336  
         }
 2337  0
     }
 2338  
 
 2339  
     /**
 2340  
      * Prints this component. This method is declared final in order to avoid unintentional
 2341  
      * overriding. Override {@link #printComponent(Graphics2D)} instead.
 2342  
      *
 2343  
      * @param graphics The graphics where to print this component.
 2344  
      */
 2345  
     @Override
 2346  
     protected final void printComponent(final Graphics graphics) {
 2347  2
         flag = IS_PRINTING;
 2348  2
         super.paintComponent(graphics);
 2349  
         /*
 2350  
          * Ne pas appeller 'super.printComponent' parce qu'on ne
 2351  
          * veut pas qu'il appelle notre 'paintComponent' ci-haut.
 2352  
          */
 2353  2
     }
 2354  
 
 2355  
     /**
 2356  
      * Returns the size (in pixels) that {@code ZoomPane} would have if it displayed the whole of
 2357  
      * the {@link #getArea} region with the current zoom ({@link #zoom}). This method is practical
 2358  
      * for determining the maximum values to assign to the scrollbars. For example, the horizontal
 2359  
      * bar could cover the range {@code [0..viewSize.width]} whilst the vertical bar could cover
 2360  
      * the range {@code [0..viewSize.height]}.
 2361  
      */
 2362  
     private Dimension getViewSize() {
 2363  7
         if (!visibleArea.isEmpty()) {
 2364  2
             Rectangle2D area = getArea();
 2365  2
             if (isValid(area)) {
 2366  2
                 area = XAffineTransform.transform(zoom, area, null);
 2367  2
                 return new Dimension((int) Math.rint(area.getWidth()),
 2368  
                                      (int) Math.rint(area.getHeight()));
 2369  
             }
 2370  0
             return getSize();
 2371  
         }
 2372  5
         return new Dimension(DEFAULT_SIZE, DEFAULT_SIZE);
 2373  
     }
 2374  
 
 2375  
     /**
 2376  
      * Returns the Insets of this component.  This method is declared final in order to avoid
 2377  
      * confusion. If you want to return other Insets you must redefine {@link #getInsets(Insets)}.
 2378  
      */
 2379  
     @Override
 2380  
     public final Insets getInsets() {
 2381  0
         return getInsets(null);
 2382  
     }
 2383  
 
 2384  
     /**
 2385  
      * Informs {@code ZoomPane} that the GUI has changed.
 2386  
      * The user doesn't have to call this method directly.
 2387  
      */
 2388  
     @Override
 2389  
     public void updateUI() {
 2390  0
         navigationPopupMenu = null;
 2391  0
         super.updateUI();
 2392  0
         setUI(UI);
 2393  0
     }
 2394  
 
 2395  
     /**
 2396  
      * Invoked when an affine transform that should be invertible is not.
 2397  
      * Default implementation logs the stack trace and resets the zoom.
 2398  
      *
 2399  
      * @param methodName The caller's method name.
 2400  
      * @param exception  The exception.
 2401  
      */
 2402  
     private void unexpectedException(String methodName, NoninvertibleTransformException exception) {
 2403  0
         zoom.setToIdentity();
 2404  0
         Logging.unexpectedException(LOGGER, ZoomPane.class, methodName, exception);
 2405  0
     }
 2406  
 
 2407  
     /**
 2408  
      * Invoked when an unexpected exception occurs.
 2409  
      * Default implementation logs the stack trace.
 2410  
      *
 2411  
      * @param methodName The caller's method name.
 2412  
      * @param exception  The exception.
 2413  
      */
 2414  
     private static void unexpectedException(String methodName, RuntimeException exception) {
 2415  0
         Logging.unexpectedException(LOGGER, ZoomPane.class, methodName, exception);
 2416  0
     }
 2417  
 
 2418  
     /**
 2419  
      * Convenience method logging an area setting from the {@code ZoomPane} class. This
 2420  
      * method is invoked from {@link #setPreferredArea} and {@link #setVisibleArea}.
 2421  
      *
 2422  
      * @param methodName The caller's method name (e.g. <code>"setArea"</code>).
 2423  
      * @param area The coordinates to log (may be {@code null}).
 2424  
      */
 2425  
     private static void log(final String methodName, final Rectangle2D area) {
 2426  3
         log(ZoomPane.class.getName(), methodName, area);
 2427  3
     }
 2428  
 
 2429  
     /**
 2430  
      * Convenience method for logging events related to area setting. Events are logged in the
 2431  
      * {@code "org.geotoolkit.gui"} logger with {@link Level#FINER}. {@code ZoomPane} invokes this
 2432  
      * method for logging any [@link #setPreferredArea} and {@link #setVisibleArea} invocations.
 2433  
      * Subclasses may invoke this method for logging some other kinds of area changes.
 2434  
      *
 2435  
      * @param className The fully qualified caller's class name
 2436  
      *        (e.g. {@code "org.geotoolkit.gui.swing.ZoomPane"}).
 2437  
      * @param methodName The caller's method name (e.g. {@code "setArea"}).
 2438  
      * @param area The coordinates to log (may be {@code null}).
 2439  
      */
 2440  
     static void log(final String className, final String methodName, final Rectangle2D area) {
 2441  3
         if (LOGGER.isLoggable(Level.FINER)) {
 2442  
             final Double[] areaBounds;
 2443  0
             if (area != null) {
 2444  0
                 areaBounds = new Double[] {
 2445  
                     area.getMinX(), area.getMaxX(),
 2446  
                     area.getMinY(), area.getMaxY()
 2447  
                 };
 2448  
             } else {
 2449  0
                 areaBounds = new Double[4];
 2450  0
                 Arrays.fill(areaBounds, new Double(Double.NaN));
 2451  
             }
 2452  0
             final Vocabulary resources = Vocabulary.getResources(null);
 2453  0
             final LogRecord record = resources.getLogRecord(Level.FINER,
 2454  
                     Vocabulary.Keys.RECTANGLE_$4, areaBounds);
 2455  0
             record.setSourceClassName (className);
 2456  0
             record.setSourceMethodName(methodName);
 2457  0
             record.setLoggerName(LOGGER.getName());
 2458  0
             LOGGER.log(record);
 2459  
         }
 2460  3
     }
 2461  
 
 2462  
     /**
 2463  
      * Checks whether the rectangle {@code rect} is valid.  The rectangle
 2464  
      * is considered invalid if its length or width is less than or equals
 2465  
      * to 0, or if one of its ordinates is infinite or NaN.
 2466  
      */
 2467  
     private static boolean isValid(final Rectangle2D rect) {
 2468  13
         if (rect == null) {
 2469  2
             return false;
 2470  
         }
 2471  11
         final double x = rect.getX();
 2472  11
         final double y = rect.getY();
 2473  11
         final double w = rect.getWidth();
 2474  11
         final double h = rect.getHeight();
 2475  11
         return (x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY &&
 2476  
                 y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY &&
 2477  
                 w > 0                        && w < Double.POSITIVE_INFINITY &&
 2478  
                 h > 0                        && h < Double.POSITIVE_INFINITY);
 2479  
     }
 2480  
 }