001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import java.awt.Component;
005import java.awt.Point;
006import java.awt.Polygon;
007import java.awt.Rectangle;
008import java.awt.event.InputEvent;
009import java.awt.event.MouseEvent;
010import java.awt.event.MouseListener;
011import java.awt.event.MouseMotionListener;
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014import java.util.Collection;
015import java.util.LinkedList;
016
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import 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 */
046public 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            @Override
126            public void propertyChange(PropertyChangeEvent evt) {
127                if (mousePosStart != null) {
128                    paintRect();
129                    mousePos = mousePosStart = null;
130                }
131            }
132        });
133    }
134    /**
135     * Unregister itself from the given event source. If a selection rectangle is
136     * shown, hide it first.
137     *
138     * @param eventSource The emitter of the mouse events.
139     */
140    public void unregister(Component eventSource) {
141        eventSource.removeMouseListener(this);
142        eventSource.removeMouseMotionListener(this);
143        selectionEndedListener.removePropertyChangeListener(this);
144    }
145
146    /**
147     * If the correct button, from the "drawing rectangle" mode
148     */
149    @Override
150    public void mousePressed(MouseEvent e) {
151        if (e.getButton() == MouseEvent.BUTTON1) {
152            mousePosStart = mousePos = e.getPoint();
153
154            lasso.reset();
155            lasso.addPoint(mousePosStart.x, mousePosStart.y);
156        }
157    }
158
159    /**
160     * If the correct button is hold, draw the rectangle.
161     */
162    @Override
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    @Override
194    public void mouseReleased(MouseEvent e) {
195        if (e.getButton() != MouseEvent.BUTTON1)
196            return;
197        if (mousePos == null || mousePosStart == null)
198            return; // injected release from outside
199        // disable the selection rect
200        Rectangle r;
201        if (!lassoMode) {
202            nc.requestClearRect();
203            r = getSelectionRectangle();
204
205            lasso = rectToPolygon(r);
206        } else {
207            nc.requestClearPoly();
208            lasso.addPoint(mousePos.x, mousePos.y);
209            r = lasso.getBounds();
210        }
211        mousePosStart = null;
212        mousePos = null;
213
214        if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) {
215            selectionEndedListener.selectionEnded(r, e);
216        }
217    }
218
219    /**
220     * Draws a selection rectangle on screen.
221     */
222    private void paintRect() {
223        if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
224            return;
225        nc.requestPaintRect(getSelectionRectangle());
226    }
227
228    private void paintLasso() {
229        if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) {
230            return;
231        }
232        lasso.addPoint(mousePos.x, mousePos.y);
233        nc.requestPaintPoly(lasso);
234    }
235
236    /**
237     * Calculate and return the current selection rectangle
238     * @return A rectangle that spans from mousePos to mouseStartPos
239     */
240    private Rectangle getSelectionRectangle() {
241        int x = mousePosStart.x;
242        int y = mousePosStart.y;
243        int w = mousePos.x - mousePosStart.x;
244        int h = mousePos.y - mousePosStart.y;
245        if (w < 0) {
246            x += w;
247            w = -w;
248        }
249        if (h < 0) {
250            y += h;
251            h = -h;
252        }
253
254        if (aspectRatio) {
255            /* Keep the aspect ratio by growing the rectangle; the
256             * rectangle is always under the cursor. */
257            double aspectRatio = (double)nc.getWidth()/nc.getHeight();
258            if ((double)w/h < aspectRatio) {
259                int neww = (int)(h*aspectRatio);
260                if (mousePos.x < mousePosStart.x) {
261                    x += w - neww;
262                }
263                w = neww;
264            } else {
265                int newh = (int)(w/aspectRatio);
266                if (mousePos.y < mousePosStart.y) {
267                    y += h - newh;
268                }
269                h = newh;
270            }
271        }
272
273        return new Rectangle(x,y,w,h);
274    }
275
276    /**
277     * If the action goes inactive, remove the selection rectangle from screen
278     */
279    @Override
280    public void propertyChange(PropertyChangeEvent evt) {
281        if (evt.getPropertyName().equals("active") && !(Boolean)evt.getNewValue() && mousePosStart != null) {
282            paintRect();
283            mousePosStart = null;
284            mousePos = null;
285        }
286    }
287
288    /**
289     * Return a list of all objects in the selection, respecting the different
290     * modifier.
291     *
292     * @param alt Whether the alt key was pressed, which means select all
293     * objects that are touched, instead those which are completely covered.
294     * @return The collection of selected objects.
295     */
296    public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
297
298        Collection<OsmPrimitive> selection = new LinkedList<OsmPrimitive>();
299
300        // whether user only clicked, not dragged.
301        boolean clicked = false;
302        Rectangle bounding = lasso.getBounds();
303        if (bounding.height <= 2 && bounding.width <= 2) {
304            clicked = true;
305        }
306
307        if (clicked) {
308            Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]);
309            OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false);
310            if (osm != null) {
311                selection.add(osm);
312            }
313        } else {
314            // nodes
315            for (Node n : nc.getCurrentDataSet().getNodes()) {
316                if (n.isSelectable() && lasso.contains(nc.getPoint2D(n))) {
317                    selection.add(n);
318                }
319            }
320
321            // ways
322            for (Way w : nc.getCurrentDataSet().getWays()) {
323                if (!w.isSelectable() || w.getNodesCount() == 0) {
324                    continue;
325                }
326                if (alt) {
327                    for (Node n : w.getNodes()) {
328                        if (!n.isIncomplete() && lasso.contains(nc.getPoint2D(n))) {
329                            selection.add(w);
330                            break;
331                        }
332                    }
333                } else {
334                    boolean allIn = true;
335                    for (Node n : w.getNodes()) {
336                        if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) {
337                            allIn = false;
338                            break;
339                        }
340                    }
341                    if (allIn) {
342                        selection.add(w);
343                    }
344                }
345            }
346        }
347        return selection;
348    }
349
350    private Polygon rectToPolygon(Rectangle r) {
351        Polygon poly = new Polygon();
352
353        poly.addPoint(r.x, r.y);
354        poly.addPoint(r.x, r.y + r.height);
355        poly.addPoint(r.x + r.width, r.y + r.height);
356        poly.addPoint(r.x + r.width, r.y);
357
358        return poly;
359    }
360
361    /**
362     * Enables or disables the lasso mode.
363     * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
364     */
365    public void setLassoMode(boolean lassoMode) {
366        this.lassoMode = lassoMode;
367    }
368
369    @Override
370    public void mouseClicked(MouseEvent e) {}
371    @Override
372    public void mouseEntered(MouseEvent e) {}
373    @Override
374    public void mouseExited(MouseEvent e) {}
375    @Override
376    public void mouseMoved(MouseEvent e) {}
377}