1 |
// Migration process to Geotools 2.6: |
2 |
// Because not included in gt2-2.6-M2 this class is taken 1:1 |
3 |
// from Geotools 2.4.5 and made public. |
4 |
// TODO: It should be removed or included in SCHMITZM. |
5 |
|
6 |
/* |
7 |
* GeoTools - OpenSource mapping toolkit |
8 |
* http://geotools.org |
9 |
* (C) 2003-2006, Geotools Project Managment Committee (PMC) |
10 |
* (C) 2001, Institut de Recherche pour le D�veloppement |
11 |
* |
12 |
* This library is free software; you can redistribute it and/or |
13 |
* modify it under the terms of the GNU Lesser General Public |
14 |
* License as published by the Free Software Foundation; either |
15 |
* version 2.1 of the License, or (at your option) any later version. |
16 |
* |
17 |
* This library is distributed in the hope that it will be useful, |
18 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
19 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
20 |
* Lesser General Public License for more details. |
21 |
*/ |
22 |
package gtmig.org.geotools.gui.swing; |
23 |
|
24 |
// Geometry |
25 |
import java.awt.Shape; |
26 |
import java.awt.Rectangle; |
27 |
import java.awt.geom.Line2D; |
28 |
import java.awt.geom.Point2D; |
29 |
import java.awt.geom.Ellipse2D; |
30 |
import java.awt.geom.Rectangle2D; |
31 |
import java.awt.geom.RoundRectangle2D; |
32 |
import java.awt.geom.RectangularShape; |
33 |
import java.awt.geom.AffineTransform; |
34 |
import java.awt.geom.NoninvertibleTransformException; |
35 |
|
36 |
// Graphics |
37 |
import java.awt.Color; |
38 |
import java.awt.Component; |
39 |
import java.awt.Graphics2D; |
40 |
|
41 |
// Events |
42 |
import java.awt.event.MouseEvent; |
43 |
import javax.swing.event.MouseInputAdapter; |
44 |
|
45 |
|
46 |
/** |
47 |
* Controller which allows the user to select a region of a component. The user must click on a |
48 |
* point in the component, then drag the mouse pointer whilst keeping the button pressed. During |
49 |
* the dragging, the shape which is drawn will normally be a rectangle. Other shapes could always |
50 |
* be used such as, for example, an ellipse. To use this class, it is necessary to create a derived |
51 |
* class which defines the following methods: |
52 |
* |
53 |
* <ul> |
54 |
* <li>{@link #selectionPerformed} (obligatory)</li> |
55 |
* <li>{@link #getModel} (optional)</li> |
56 |
* </ul> |
57 |
* |
58 |
* This controller should then be registered with one, and only one, component |
59 |
* using the following syntax: |
60 |
* |
61 |
* <blockquote><pre> |
62 |
* {@link Component} component=... |
63 |
* MouseSelectionTracker control=... |
64 |
* component.addMouseListener(control); |
65 |
* </pre></blockquote> |
66 |
* |
67 |
* @since 2.0 |
68 |
* @source $URL: http://svn.geotools.org/tags/2.4.5/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/MouseSelectionTracker.java $ |
69 |
* @version $Id: MouseSelectionTracker.java 22482 2006-10-31 02:58:00Z desruisseaux $ |
70 |
* @author Martin Desruisseaux |
71 |
*/ |
72 |
public abstract class MouseSelectionTracker extends MouseInputAdapter { |
73 |
/** |
74 |
* Stippled rectangle representing the region which the user is currently |
75 |
* selecting. This rectangle can be empty. These coordinates are only |
76 |
* significant in the period between the user pressing the mouse button |
77 |
* and then releasing it to outline a region. Conventionally, the |
78 |
* {@code null} value indicates that a line should be used instead of |
79 |
* a rectangular shape. The coordinates are always expressed in pixels. |
80 |
*/ |
81 |
private transient RectangularShape mouseSelectedArea; |
82 |
|
83 |
/** |
84 |
* Colour to replace during XOR drawings on a graphic. |
85 |
* This colour is specified in {@link Graphics2D#setColor}. |
86 |
*/ |
87 |
private Color backXORColor = Color.white; |
88 |
|
89 |
/** |
90 |
* Colour to replace with during the XOR drawings on a graphic. |
91 |
* This colour is specified in {@link Graphics2D#setXORMode}. |
92 |
*/ |
93 |
private Color lineXORColor = Color.black; |
94 |
|
95 |
/** |
96 |
* <var>x</var> coordinate of the mouse when the button is pressed. |
97 |
*/ |
98 |
private transient int ox; |
99 |
|
100 |
/** |
101 |
* <var>y</var> coordinate of the mouse when the button is pressed. |
102 |
*/ |
103 |
private transient int oy; |
104 |
|
105 |
/** |
106 |
* <var>x</var> coordinate of the mouse during the last drag. |
107 |
*/ |
108 |
private transient int px; |
109 |
|
110 |
/** |
111 |
* <var>y</var> coordinate of the mouse during the last drag. |
112 |
*/ |
113 |
private transient int py; |
114 |
|
115 |
/** |
116 |
* Indicates whether a selection is underway. |
117 |
*/ |
118 |
private transient boolean isDragging; |
119 |
|
120 |
/** |
121 |
* Constructs an object which will allow rectangular regions to be selected using the mouse. |
122 |
*/ |
123 |
public MouseSelectionTracker() { |
124 |
} |
125 |
|
126 |
/** |
127 |
* Specifies the colours to be used for drawing the outline of a box when |
128 |
* the user selects a region. All {@code a} colours will be replaced |
129 |
* by {@code b} colours and vice versa. |
130 |
*/ |
131 |
public void setXORColors(final Color a, final Color b) { |
132 |
backXORColor = a; |
133 |
lineXORColor = b; |
134 |
} |
135 |
|
136 |
/** |
137 |
* Returns the geometric shape to use for marking the boundaries of a region. This shape is |
138 |
* normally a rectangle but could also be an ellipse, an arrow or even other shapes. The |
139 |
* coordinates of the returned shape will not be taken into account. In fact, these coordinates |
140 |
* will regularly be discarded. Only the class of the returned shape will count (for example, |
141 |
* {@link java.awt.geom.Ellipse2D} vs {@link java.awt.geom.Rectangle2D}) and their parameters |
142 |
* which are not linked to their position (for example, the rounding of a rectangle's |
143 |
* corners). |
144 |
* <p> |
145 |
* The shape returned will normally be from a class derived from {@link RectangularShape}, |
146 |
* but could also be from the {@link Line2D} class. <strong>Any other class risks throwing a |
147 |
* {@link ClassCastException} when executed</strong>. |
148 |
* |
149 |
* The default implementation always returns an object {@link Rectangle}. |
150 |
* |
151 |
* @param event Mouse coordinate when the button is pressed. This information can be used by |
152 |
* the derived classes which like to be informed of the position of the mouse before |
153 |
* chosing a geometric shape. |
154 |
* @return Shape from the class {link RectangularShape} or {link Line2D}, or {@code null} |
155 |
* to indicate that we do not want to make a selection. |
156 |
*/ |
157 |
protected Shape getModel(final MouseEvent event) { |
158 |
return new Rectangle(); |
159 |
} |
160 |
|
161 |
/** |
162 |
* Method which is automatically called after the user selects a region with the mouse. |
163 |
* All coordinates passed in as parameters are expressed in pixels. |
164 |
* |
165 |
* @param ox <var>x</var> coordinate of the mouse when the user pressed the mouse button. |
166 |
* @param oy <var>y</var> coordinate of the mouse when the user pressed the mouse button. |
167 |
* @param px <var>x</var> coordinate of the mouse when the user released the mouse button. |
168 |
* @param py <var>y</var> coordinate of the mouse when the user released the mouse button. |
169 |
*/ |
170 |
protected abstract void selectionPerformed(int ox, int oy, int px, int py); |
171 |
|
172 |
/** |
173 |
* Returns the geometric shape surrounding the last region to be selected by the user. An |
174 |
* optional affine transform can be specified to convert the region selected by the user |
175 |
* into logical coordinates. The class of the shape returned depends on the model returned by |
176 |
* {@link #getModel}: |
177 |
* |
178 |
* <ul> |
179 |
* <li>If the model is null (which means that this {@code MouseSelectionTracker} object only |
180 |
* draws a line between points), the object returned will belong to the {@link Line2D} |
181 |
* class.</li> |
182 |
* <li>If the model is not null, the object returned can be from the same class (most often |
183 |
* {@link java.awt.geom.Rectangle2D}). There could always be situations where the object |
184 |
* returned is from another class, for example if the affine transform carries out a |
185 |
* rotation.</li> |
186 |
* </ul> |
187 |
* |
188 |
* @param transform Affine transform which converts logical coordinates into pixel coordinates. |
189 |
* It is usually an affine transform which is used in a {@code paint(...)} method to |
190 |
* draw shapes expressed in logical coordinates. |
191 |
* @return A geometric shape enclosing the last region to be selected by the user, or |
192 |
* {@code null} if no selection has yet been made. |
193 |
* @throws NoninvertibleTransformException If the affine transform can't be inverted. |
194 |
*/ |
195 |
public Shape getSelectedArea(final AffineTransform transform) throws NoninvertibleTransformException { |
196 |
if (ox == px && oy == py) { |
197 |
return null; |
198 |
} |
199 |
RectangularShape shape = mouseSelectedArea; |
200 |
if (transform != null && !transform.isIdentity()) { |
201 |
if (shape == null) { |
202 |
final Point2D.Float po = new Point2D.Float(ox, oy); |
203 |
final Point2D.Float pp = new Point2D.Float(px, py); |
204 |
transform.inverseTransform(po, po); |
205 |
transform.inverseTransform(pp, pp); |
206 |
return new Line2D.Float(po, pp); |
207 |
} else { |
208 |
if (canReshape(shape, transform)) { |
209 |
final Point2D.Double point = new Point2D.Double(); |
210 |
double xmin = Double.POSITIVE_INFINITY; |
211 |
double ymin = Double.POSITIVE_INFINITY; |
212 |
double xmax = Double.NEGATIVE_INFINITY; |
213 |
double ymax = Double.NEGATIVE_INFINITY; |
214 |
for (int i = 0; i < 4; i++) { |
215 |
point.x = (i&1) == 0 ? shape.getMinX() : shape.getMaxX(); |
216 |
point.y = (i&2) == 0 ? shape.getMinY() : shape.getMaxY(); |
217 |
transform.inverseTransform(point, point); |
218 |
if (point.x < xmin) xmin = point.x; |
219 |
if (point.x > xmax) xmax = point.x; |
220 |
if (point.y < ymin) ymin = point.y; |
221 |
if (point.y > ymax) ymax = point.y; |
222 |
} |
223 |
if (shape instanceof Rectangle) { |
224 |
return new Rectangle2D.Float((float) xmin, |
225 |
(float) ymin, |
226 |
(float) (xmax - xmin), |
227 |
(float) (ymax - ymin)); |
228 |
} else { |
229 |
shape = (RectangularShape) shape.clone(); |
230 |
shape.setFrame(xmin, ymin, xmax - xmin, ymax - ymin); |
231 |
return shape; |
232 |
} |
233 |
} |
234 |
else { |
235 |
return transform.createInverse().createTransformedShape(shape); |
236 |
} |
237 |
} |
238 |
} |
239 |
else { |
240 |
return (shape != null) ? (Shape) shape.clone() : new Line2D.Float(ox, oy, px, py); |
241 |
} |
242 |
} |
243 |
|
244 |
/** |
245 |
* Indicates whether we can transform {@code shape} simply by calling its |
246 |
* {@code shape.setFrame(...)} method rather than by using the heavy artillery |
247 |
* that is the {@code transform.createTransformedShape(shape)} method. |
248 |
*/ |
249 |
private static boolean canReshape(final RectangularShape shape, |
250 |
final AffineTransform transform) { |
251 |
final int type=transform.getType(); |
252 |
if ((type & AffineTransform.TYPE_GENERAL_TRANSFORM) != 0) return false; |
253 |
if ((type & AffineTransform.TYPE_MASK_ROTATION) != 0) return false; |
254 |
if ((type & AffineTransform.TYPE_FLIP) != 0) { |
255 |
if (shape instanceof Rectangle2D) return true; |
256 |
if (shape instanceof Ellipse2D) return true; |
257 |
if (shape instanceof RoundRectangle2D) return true; |
258 |
return false; |
259 |
} |
260 |
return true; |
261 |
} |
262 |
|
263 |
/** |
264 |
* Returns a {@link Graphics2D} object to be used for drawing in the specified component. We |
265 |
* must not forget to call {@link Graphics2D#dispose} when the graphics object is no longer |
266 |
* needed. |
267 |
*/ |
268 |
private Graphics2D getGraphics(final Component c) { |
269 |
final Graphics2D graphics = (Graphics2D) c.getGraphics(); |
270 |
graphics.setXORMode(lineXORColor); |
271 |
graphics.setColor (backXORColor); |
272 |
return graphics; |
273 |
} |
274 |
|
275 |
/** |
276 |
* Informs this controller that the mouse button has been pressed. |
277 |
* The default implementation retains the mouse coordinate (which will |
278 |
* become one of the corners of the future rectangle to be drawn) |
279 |
* and prepares {@code this} to observe the mouse movements. |
280 |
* |
281 |
* @throws ClassCastException if {@link #getModel} doesn't return a shape |
282 |
* from the class {link RectangularShape} or {link Line2D}. |
283 |
*/ |
284 |
public void mousePressed(final MouseEvent event) throws ClassCastException { |
285 |
if (!event.isConsumed() && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { |
286 |
final Component source = event.getComponent(); |
287 |
if (source != null) { |
288 |
Shape model = getModel(event); |
289 |
if (model != null) { |
290 |
isDragging = true; |
291 |
ox = px = event.getX(); |
292 |
oy = py = event.getY(); |
293 |
if (model instanceof Line2D) { |
294 |
model = null; |
295 |
} |
296 |
mouseSelectedArea = (RectangularShape) model; |
297 |
if (mouseSelectedArea != null) { |
298 |
mouseSelectedArea.setFrame(ox, oy, 0, 0); |
299 |
} |
300 |
source.addMouseMotionListener(this); |
301 |
} |
302 |
source.requestFocus(); |
303 |
event.consume(); |
304 |
} |
305 |
} |
306 |
} |
307 |
|
308 |
/** |
309 |
* Informs this controller that the mouse has been dragged. The default |
310 |
* implementation uses this to move a corner of the rectangle used to |
311 |
* select the region. The other corner remains fixed at the point |
312 |
* where the mouse was at the moment its button was pressed.. |
313 |
*/ |
314 |
public void mouseDragged(final MouseEvent event) { |
315 |
if (isDragging) { |
316 |
final Graphics2D graphics = getGraphics(event.getComponent()); |
317 |
if (mouseSelectedArea == null) { |
318 |
graphics.drawLine(ox, oy, px, py); |
319 |
px = event.getX(); |
320 |
py = event.getY(); |
321 |
graphics.drawLine(ox, oy, px, py); |
322 |
} else { |
323 |
graphics.draw(mouseSelectedArea); |
324 |
int xmin = this.ox; |
325 |
int ymin = this.oy; |
326 |
int xmax = px = event.getX(); |
327 |
int ymax = py = event.getY(); |
328 |
if (xmin > xmax) { |
329 |
final int xtmp = xmin; |
330 |
xmin = xmax; xmax = xtmp; |
331 |
} |
332 |
if (ymin > ymax) { |
333 |
final int ytmp = ymin; |
334 |
ymin = ymax; ymax = ytmp; |
335 |
} |
336 |
mouseSelectedArea.setFrame(xmin, ymin, xmax - xmin, ymax - ymin); |
337 |
graphics.draw(mouseSelectedArea); |
338 |
} |
339 |
graphics.dispose(); |
340 |
event.consume(); |
341 |
} |
342 |
} |
343 |
|
344 |
/** |
345 |
* Informs this controller that the mouse button has been released. |
346 |
* The default implementation calls {@link #selectionPerformed} with |
347 |
* the bounds of the selected region as parameters. |
348 |
*/ |
349 |
public void mouseReleased(final MouseEvent event) { |
350 |
if (isDragging && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { |
351 |
isDragging = false; |
352 |
final Component component = event.getComponent(); |
353 |
component.removeMouseMotionListener(this); |
354 |
|
355 |
final Graphics2D graphics = getGraphics(event.getComponent()); |
356 |
if (mouseSelectedArea == null) { |
357 |
graphics.drawLine(ox, oy, px, py); |
358 |
} else { |
359 |
graphics.draw(mouseSelectedArea); |
360 |
} |
361 |
graphics.dispose(); |
362 |
px = event.getX(); |
363 |
py = event.getY(); |
364 |
selectionPerformed(ox, oy, px, py); |
365 |
event.consume(); |
366 |
} |
367 |
} |
368 |
|
369 |
/** |
370 |
* Informs this controller that the mouse has been moved but not as a |
371 |
* result of the user selecting a region. The default implementation |
372 |
* signals to the source component that {@code this} is no longer |
373 |
* interested in being informed about mouse movements. |
374 |
*/ |
375 |
public void mouseMoved(final MouseEvent event) { |
376 |
// Normally not necessary, but it seems that this "listener" |
377 |
// sometimes stays in place when it shouldn't. |
378 |
event.getComponent().removeMouseMotionListener(this); |
379 |
} |
380 |
} |