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