001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui;
003    
004    import java.awt.Component;
005    import java.awt.Point;
006    import java.awt.Polygon;
007    import java.awt.Rectangle;
008    import java.awt.event.InputEvent;
009    import java.awt.event.MouseEvent;
010    import java.awt.event.MouseListener;
011    import java.awt.event.MouseMotionListener;
012    import java.beans.PropertyChangeEvent;
013    import java.beans.PropertyChangeListener;
014    import java.util.Collection;
015    import java.util.LinkedList;
016    
017    import org.openstreetmap.josm.data.osm.Node;
018    import org.openstreetmap.josm.data.osm.OsmPrimitive;
019    import org.openstreetmap.josm.data.osm.Way;
020    
021    /**
022     * Manages the selection of a rectangle. Listening to left and right mouse button
023     * presses and to mouse motions and draw the rectangle accordingly.
024     *
025     * Left mouse button selects a rectangle from the press until release. Pressing
026     * right mouse button while left is still pressed enable the rectangle to move
027     * around. Releasing the left button fires an action event to the listener given
028     * at constructor, except if the right is still pressed, which just remove the
029     * selection rectangle and does nothing.
030     *
031     * The point where the left mouse button was pressed and the current mouse
032     * position are two opposite corners of the selection rectangle.
033     *
034     * It is possible to specify an aspect ratio (width per height) which the
035     * selection rectangle always must have. In this case, the selection rectangle
036     * will be the largest window with this aspect ratio, where the position the left
037     * mouse button was pressed and the corner of the current mouse position are at
038     * opposite sites (the mouse position corner is the corner nearest to the mouse
039     * cursor).
040     *
041     * When the left mouse button was released, an ActionEvent is send to the
042     * ActionListener given at constructor. The source of this event is this manager.
043     *
044     * @author imi
045     */
046    public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener {
047    
048        /**
049         * This is the interface that an user of SelectionManager has to implement
050         * to get informed when a selection closes.
051         * @author imi
052         */
053        public interface SelectionEnded {
054            /**
055             * Called, when the left mouse button was released.
056             * @param r The rectangle that is currently the selection.
057             * @param e The mouse event.
058             * @see InputEvent#getModifiersEx()
059             */
060            public void selectionEnded(Rectangle r, MouseEvent e);
061            /**
062             * Called to register the selection manager for "active" property.
063             * @param listener The listener to register
064             */
065            public void addPropertyChangeListener(PropertyChangeListener listener);
066            /**
067             * Called to remove the selection manager from the listener list
068             * for "active" property.
069             * @param listener The listener to register
070             */
071            public void removePropertyChangeListener(PropertyChangeListener listener);
072        }
073        /**
074         * The listener that receives the events after left mouse button is released.
075         */
076        private final SelectionEnded selectionEndedListener;
077        /**
078         * Position of the map when the mouse button was pressed.
079         * If this is not <code>null</code>, a rectangle is drawn on screen.
080         */
081        private Point mousePosStart;
082        /**
083         * Position of the map when the selection rectangle was last drawn.
084         */
085        private Point mousePos;
086        /**
087         * The Component, the selection rectangle is drawn onto.
088         */
089        private final NavigatableComponent nc;
090        /**
091         * Whether the selection rectangle must obtain the aspect ratio of the
092         * drawComponent.
093         */
094        private boolean aspectRatio;
095    
096        private boolean lassoMode;
097        private Polygon lasso = new Polygon();
098    
099        /**
100         * Create a new SelectionManager.
101         *
102         * @param selectionEndedListener The action listener that receives the event when
103         *      the left button is released.
104         * @param aspectRatio If true, the selection window must obtain the aspect
105         *      ratio of the drawComponent.
106         * @param navComp The component, the rectangle is drawn onto.
107         */
108        public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
109            this.selectionEndedListener = selectionEndedListener;
110            this.aspectRatio = aspectRatio;
111            this.nc = navComp;
112        }
113    
114        /**
115         * Register itself at the given event source.
116         * @param eventSource The emitter of the mouse events.
117         * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
118         */
119        public void register(NavigatableComponent eventSource, boolean lassoMode) {
120           this.lassoMode = lassoMode;
121            eventSource.addMouseListener(this);
122            eventSource.addMouseMotionListener(this);
123            selectionEndedListener.addPropertyChangeListener(this);
124            eventSource.addPropertyChangeListener("scale", new PropertyChangeListener(){
125                public void propertyChange(PropertyChangeEvent evt) {
126                    if (mousePosStart != null) {
127                        paintRect();
128                        mousePos = mousePosStart = null;
129                    }
130                }
131            });
132        }
133        /**
134         * Unregister itself from the given event source. If a selection rectangle is
135         * shown, hide it first.
136         *
137         * @param eventSource The emitter of the mouse events.
138         */
139        public void unregister(Component eventSource) {
140            eventSource.removeMouseListener(this);
141            eventSource.removeMouseMotionListener(this);
142            selectionEndedListener.removePropertyChangeListener(this);
143        }
144    
145        /**
146         * If the correct button, from the "drawing rectangle" mode
147         */
148        public void mousePressed(MouseEvent e) {
149            if (e.getButton() == MouseEvent.BUTTON1) {
150                mousePosStart = mousePos = e.getPoint();
151    
152                lasso.reset();
153                lasso.addPoint(mousePosStart.x, mousePosStart.y);
154            }
155        }
156    
157        /**
158         * If the correct button is hold, draw the rectangle.
159         */
160        public void mouseDragged(MouseEvent e) {
161            int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK);
162    
163            if (buttonPressed != 0) {
164                if (mousePosStart == null) {
165                    mousePosStart = mousePos = e.getPoint();
166                }
167                if (!lassoMode) {
168                    paintRect();
169                }
170            }
171    
172            if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) {
173                mousePos = e.getPoint();
174                if (lassoMode) {
175                    paintLasso();
176                } else {
177                    paintRect();
178                }
179            } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) {
180                mousePosStart.x += e.getX()-mousePos.x;
181                mousePosStart.y += e.getY()-mousePos.y;
182                mousePos = e.getPoint();
183                paintRect();
184            }
185        }
186    
187        /**
188         * Check the state of the keys and buttons and set the selection accordingly.
189         */
190        public void mouseReleased(MouseEvent e) {
191            if (e.getButton() != MouseEvent.BUTTON1)
192                return;
193            if (mousePos == null || mousePosStart == null)
194                return; // injected release from outside
195            // disable the selection rect
196            Rectangle r;
197            if (!lassoMode) {
198                nc.requestClearRect();
199                r = getSelectionRectangle();
200    
201                lasso = rectToPolygon(r);
202            } else {
203                nc.requestClearPoly();
204                lasso.addPoint(mousePos.x, mousePos.y);
205                r = lasso.getBounds();
206            }
207            mousePosStart = null;
208            mousePos = null;
209    
210            if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) {
211                selectionEndedListener.selectionEnded(r, e);
212            }
213        }
214    
215        /**
216         * Draws a selection rectangle on screen.
217         */
218        private void paintRect() {
219            if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
220                return;
221            nc.requestPaintRect(getSelectionRectangle());
222        }
223        
224        private void paintLasso() {
225            if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) {
226                return;
227            }
228            lasso.addPoint(mousePos.x, mousePos.y);
229            nc.requestPaintPoly(lasso);
230        }
231    
232        /**
233         * Calculate and return the current selection rectangle
234         * @return A rectangle that spans from mousePos to mouseStartPos
235         */
236        private Rectangle getSelectionRectangle() {
237            int x = mousePosStart.x;
238            int y = mousePosStart.y;
239            int w = mousePos.x - mousePosStart.x;
240            int h = mousePos.y - mousePosStart.y;
241            if (w < 0) {
242                x += w;
243                w = -w;
244            }
245            if (h < 0) {
246                y += h;
247                h = -h;
248            }
249    
250            if (aspectRatio) {
251                /* Keep the aspect ratio by growing the rectangle; the
252                 * rectangle is always under the cursor. */
253                double aspectRatio = (double)nc.getWidth()/nc.getHeight();
254                if ((double)w/h < aspectRatio) {
255                    int neww = (int)(h*aspectRatio);
256                    if (mousePos.x < mousePosStart.x) {
257                        x += w - neww;
258                    }
259                    w = neww;
260                } else {
261                    int newh = (int)(w/aspectRatio);
262                    if (mousePos.y < mousePosStart.y) {
263                        y += h - newh;
264                    }
265                    h = newh;
266                }
267            }
268    
269            return new Rectangle(x,y,w,h);
270        }
271    
272        /**
273         * If the action goes inactive, remove the selection rectangle from screen
274         */
275        public void propertyChange(PropertyChangeEvent evt) {
276            if (evt.getPropertyName().equals("active") && !(Boolean)evt.getNewValue() && mousePosStart != null) {
277                paintRect();
278                mousePosStart = null;
279                mousePos = null;
280            }
281        }
282    
283        /**
284         * Return a list of all objects in the selection, respecting the different
285         * modifier.
286         *
287         * @param alt Whether the alt key was pressed, which means select all
288         * objects that are touched, instead those which are completely covered.
289         * @return The collection of selected objects.
290         */
291        public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
292    
293            Collection<OsmPrimitive> selection = new LinkedList<OsmPrimitive>();
294    
295            // whether user only clicked, not dragged.
296            boolean clicked = false;
297            Rectangle bounding = lasso.getBounds();
298            if (bounding.height <= 2 && bounding.width <= 2) {
299                clicked = true;
300            }
301    
302            if (clicked) {
303                Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]);
304                OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false);
305                if (osm != null) {
306                    selection.add(osm);
307                }
308            } else {
309                // nodes
310                for (Node n : nc.getCurrentDataSet().getNodes()) {
311                    if (n.isSelectable() && lasso.contains(nc.getPoint(n))) {
312                        selection.add(n);
313                    }
314                }
315    
316                // ways
317                for (Way w : nc.getCurrentDataSet().getWays()) {
318                    if (!w.isSelectable() || w.getNodesCount() == 0) {
319                        continue;
320                    }
321                    if (alt) {
322                        for (Node n : w.getNodes()) {
323                            if (!n.isIncomplete() && lasso.contains(nc.getPoint(n))) {
324                                selection.add(w);
325                                break;
326                            }
327                        }
328                    } else {
329                        boolean allIn = true;
330                        for (Node n : w.getNodes()) {
331                            if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) {
332                                allIn = false;
333                                break;
334                            }
335                        }
336                        if (allIn) {
337                            selection.add(w);
338                        }
339                    }
340                }
341            }
342            return selection;
343        }
344    
345        private Polygon rectToPolygon(Rectangle r) {
346            Polygon poly = new Polygon();
347    
348            poly.addPoint(r.x, r.y);
349            poly.addPoint(r.x, r.y + r.height);
350            poly.addPoint(r.x + r.width, r.y + r.height);
351            poly.addPoint(r.x + r.width, r.y);
352    
353            return poly;
354        }
355    
356        /**
357         * Enables or disables the lasso mode.
358         * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
359         */
360        public void setLassoMode(boolean lassoMode) {
361            this.lassoMode = lassoMode;
362        }
363    
364        public void mouseClicked(MouseEvent e) {}
365        public void mouseEntered(MouseEvent e) {}
366        public void mouseExited(MouseEvent e) {}
367        public void mouseMoved(MouseEvent e) {}
368    }