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