Source Code Cross Referenced for ZoomPane.java in  » GIS » GeoTools-2.4.1 » org » geotools » gui » swing » Java Source Code / Java DocumentationJava Source Code and Java Documentation

Java Source Code / Java Documentation
1. 6.0 JDK Core
2. 6.0 JDK Modules
3. 6.0 JDK Modules com.sun
4. 6.0 JDK Modules com.sun.java
5. 6.0 JDK Modules sun
6. 6.0 JDK Platform
7. Ajax
8. Apache Harmony Java SE
9. Aspect oriented
10. Authentication Authorization
11. Blogger System
12. Build
13. Byte Code
14. Cache
15. Chart
16. Chat
17. Code Analyzer
18. Collaboration
19. Content Management System
20. Database Client
21. Database DBMS
22. Database JDBC Connection Pool
23. Database ORM
24. Development
25. EJB Server geronimo
26. EJB Server GlassFish
27. EJB Server JBoss 4.2.1
28. EJB Server resin 3.1.5
29. ERP CRM Financial
30. ESB
31. Forum
32. GIS
33. Graphic Library
34. Groupware
35. HTML Parser
36. IDE
37. IDE Eclipse
38. IDE Netbeans
39. Installer
40. Internationalization Localization
41. Inversion of Control
42. Issue Tracking
43. J2EE
44. JBoss
45. JMS
46. JMX
47. Library
48. Mail Clients
49. Net
50. Parser
51. PDF
52. Portal
53. Profiler
54. Project Management
55. Report
56. RSS RDF
57. Rule Engine
58. Science
59. Scripting
60. Search Engine
61. Security
62. Sevlet Container
63. Source Control
64. Swing Library
65. Template Engine
66. Test Coverage
67. Testing
68. UML
69. Web Crawler
70. Web Framework
71. Web Mail
72. Web Server
73. Web Services
74. Web Services apache cxf 2.0.1
75. Web Services AXIS2
76. Wiki Engine
77. Workflow Engines
78. XML
79. XML UI
Java
Java Tutorial
Java Open Source
Jar File Download
Java Articles
Java Products
Java by API
Photoshop Tutorials
Maya Tutorials
Flash Tutorials
3ds-Max Tutorials
Illustrator Tutorials
GIMP Tutorials
C# / C Sharp
C# / CSharp Tutorial
C# / CSharp Open Source
ASP.Net
ASP.NET Tutorial
JavaScript DHTML
JavaScript Tutorial
JavaScript Reference
HTML / CSS
HTML CSS Reference
C / ANSI-C
C Tutorial
C++
C++ Tutorial
Ruby
PHP
Python
Python Tutorial
Python Open Source
SQL Server / T-SQL
SQL Server / T-SQL Tutorial
Oracle PL / SQL
Oracle PL/SQL Tutorial
PostgreSQL
SQL / MySQL
MySQL Tutorial
VB.Net
VB.Net Tutorial
Flash / Flex / ActionScript
VBA / Excel / Access / Word
XML
XML Tutorial
Microsoft Office PowerPoint 2007 Tutorial
Microsoft Office Excel 2007 Tutorial
Microsoft Office Word 2007 Tutorial
Java Source Code / Java Documentation » GIS » GeoTools 2.4.1 » org.geotools.gui.swing 
Source Cross Referenced  Class Diagram Java Document (Java Doc) 


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