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