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.Main;
018import org.openstreetmap.josm.actions.SelectByInternalPointAction;
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import 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 */
048public 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 e The mouse event.
060         * @see InputEvent#getModifiersEx()
061         */
062        public void selectionEnded(Rectangle r, MouseEvent e);
063        /**
064         * Called to register the selection manager for "active" property.
065         * @param listener The listener to register
066         */
067        public void addPropertyChangeListener(PropertyChangeListener listener);
068        /**
069         * Called to remove the selection manager from the listener list
070         * for "active" property.
071         * @param listener The listener to register
072         */
073        public void removePropertyChangeListener(PropertyChangeListener listener);
074    }
075    /**
076     * The listener that receives the events after left mouse button is released.
077     */
078    private final SelectionEnded selectionEndedListener;
079    /**
080     * Position of the map when the mouse button was pressed.
081     * If this is not <code>null</code>, a rectangle is drawn on screen.
082     */
083    private Point mousePosStart;
084    /**
085     * Position of the map when the selection rectangle was last drawn.
086     */
087    private Point mousePos;
088    /**
089     * The Component, the selection rectangle is drawn onto.
090     */
091    private final NavigatableComponent nc;
092    /**
093     * Whether the selection rectangle must obtain the aspect ratio of the
094     * drawComponent.
095     */
096    private boolean aspectRatio;
097
098    private boolean lassoMode;
099    private Polygon lasso = new Polygon();
100
101    /**
102     * Create a new SelectionManager.
103     *
104     * @param selectionEndedListener The action listener that receives the event when
105     *      the left button is released.
106     * @param aspectRatio If true, the selection window must obtain the aspect
107     *      ratio of the drawComponent.
108     * @param navComp The component, the rectangle is drawn onto.
109     */
110    public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
111        this.selectionEndedListener = selectionEndedListener;
112        this.aspectRatio = aspectRatio;
113        this.nc = navComp;
114    }
115
116    /**
117     * Register itself at the given event source.
118     * @param eventSource The emitter of the mouse events.
119     * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
120     */
121    public void register(NavigatableComponent eventSource, boolean lassoMode) {
122       this.lassoMode = lassoMode;
123        eventSource.addMouseListener(this);
124        eventSource.addMouseMotionListener(this);
125        selectionEndedListener.addPropertyChangeListener(this);
126        eventSource.addPropertyChangeListener("scale", new PropertyChangeListener(){
127            @Override
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    @Override
152    public void mousePressed(MouseEvent e) {
153        if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && Main.main.getCurrentDataSet() != null) {
154            SelectByInternalPointAction.performSelection(Main.map.mapView.getEastNorth(e.getX(), e.getY()),
155                    (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) > 0,
156                    (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0);
157        } else if (e.getButton() == MouseEvent.BUTTON1) {
158            mousePosStart = mousePos = e.getPoint();
159
160            lasso.reset();
161            lasso.addPoint(mousePosStart.x, mousePosStart.y);
162        }
163    }
164
165    /**
166     * If the correct button is hold, draw the rectangle.
167     */
168    @Override
169    public void mouseDragged(MouseEvent e) {
170        int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK);
171
172        if (buttonPressed != 0) {
173            if (mousePosStart == null) {
174                mousePosStart = mousePos = e.getPoint();
175            }
176            if (!lassoMode) {
177                paintRect();
178            }
179        }
180
181        if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) {
182            mousePos = e.getPoint();
183            if (lassoMode) {
184                paintLasso();
185            } else {
186                paintRect();
187            }
188        } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) {
189            mousePosStart.x += e.getX()-mousePos.x;
190            mousePosStart.y += e.getY()-mousePos.y;
191            mousePos = e.getPoint();
192            paintRect();
193        }
194    }
195
196    /**
197     * Check the state of the keys and buttons and set the selection accordingly.
198     */
199    @Override
200    public void mouseReleased(MouseEvent e) {
201        if (e.getButton() != MouseEvent.BUTTON1)
202            return;
203        if (mousePos == null || mousePosStart == null)
204            return; // injected release from outside
205        // disable the selection rect
206        Rectangle r;
207        if (!lassoMode) {
208            nc.requestClearRect();
209            r = getSelectionRectangle();
210
211            lasso = rectToPolygon(r);
212        } else {
213            nc.requestClearPoly();
214            lasso.addPoint(mousePos.x, mousePos.y);
215            r = lasso.getBounds();
216        }
217        mousePosStart = null;
218        mousePos = null;
219
220        if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) {
221            selectionEndedListener.selectionEnded(r, e);
222        }
223    }
224
225    /**
226     * Draws a selection rectangle on screen.
227     */
228    private void paintRect() {
229        if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
230            return;
231        nc.requestPaintRect(getSelectionRectangle());
232    }
233
234    private void paintLasso() {
235        if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) {
236            return;
237        }
238        lasso.addPoint(mousePos.x, mousePos.y);
239        nc.requestPaintPoly(lasso);
240    }
241
242    /**
243     * Calculate and return the current selection rectangle
244     * @return A rectangle that spans from mousePos to mouseStartPos
245     */
246    private Rectangle getSelectionRectangle() {
247        int x = mousePosStart.x;
248        int y = mousePosStart.y;
249        int w = mousePos.x - mousePosStart.x;
250        int h = mousePos.y - mousePosStart.y;
251        if (w < 0) {
252            x += w;
253            w = -w;
254        }
255        if (h < 0) {
256            y += h;
257            h = -h;
258        }
259
260        if (aspectRatio) {
261            /* Keep the aspect ratio by growing the rectangle; the
262             * rectangle is always under the cursor. */
263            double aspectRatio = (double)nc.getWidth()/nc.getHeight();
264            if ((double)w/h < aspectRatio) {
265                int neww = (int)(h*aspectRatio);
266                if (mousePos.x < mousePosStart.x) {
267                    x += w - neww;
268                }
269                w = neww;
270            } else {
271                int newh = (int)(w/aspectRatio);
272                if (mousePos.y < mousePosStart.y) {
273                    y += h - newh;
274                }
275                h = newh;
276            }
277        }
278
279        return new Rectangle(x,y,w,h);
280    }
281
282    /**
283     * If the action goes inactive, remove the selection rectangle from screen
284     */
285    @Override
286    public void propertyChange(PropertyChangeEvent evt) {
287        if ("active".equals(evt.getPropertyName()) && !(Boolean)evt.getNewValue() && mousePosStart != null) {
288            paintRect();
289            mousePosStart = null;
290            mousePos = null;
291        }
292    }
293
294    /**
295     * Return a list of all objects in the selection, respecting the different
296     * modifier.
297     *
298     * @param alt Whether the alt key was pressed, which means select all
299     * objects that are touched, instead those which are completely covered.
300     * @return The collection of selected objects.
301     */
302    public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
303
304        Collection<OsmPrimitive> selection = new LinkedList<>();
305
306        // whether user only clicked, not dragged.
307        boolean clicked = false;
308        Rectangle bounding = lasso.getBounds();
309        if (bounding.height <= 2 && bounding.width <= 2) {
310            clicked = true;
311        }
312
313        if (clicked) {
314            Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]);
315            OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false);
316            if (osm != null) {
317                selection.add(osm);
318            }
319        } else {
320            // nodes
321            for (Node n : nc.getCurrentDataSet().getNodes()) {
322                if (n.isSelectable() && lasso.contains(nc.getPoint2D(n))) {
323                    selection.add(n);
324                }
325            }
326
327            // ways
328            for (Way w : nc.getCurrentDataSet().getWays()) {
329                if (!w.isSelectable() || w.getNodesCount() == 0) {
330                    continue;
331                }
332                if (alt) {
333                    for (Node n : w.getNodes()) {
334                        if (!n.isIncomplete() && lasso.contains(nc.getPoint2D(n))) {
335                            selection.add(w);
336                            break;
337                        }
338                    }
339                } else {
340                    boolean allIn = true;
341                    for (Node n : w.getNodes()) {
342                        if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) {
343                            allIn = false;
344                            break;
345                        }
346                    }
347                    if (allIn) {
348                        selection.add(w);
349                    }
350                }
351            }
352        }
353        return selection;
354    }
355
356    private Polygon rectToPolygon(Rectangle r) {
357        Polygon poly = new Polygon();
358
359        poly.addPoint(r.x, r.y);
360        poly.addPoint(r.x, r.y + r.height);
361        poly.addPoint(r.x + r.width, r.y + r.height);
362        poly.addPoint(r.x + r.width, r.y);
363
364        return poly;
365    }
366
367    /**
368     * Enables or disables the lasso mode.
369     * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
370     */
371    public void setLassoMode(boolean lassoMode) {
372        this.lassoMode = lassoMode;
373    }
374
375    @Override
376    public void mouseClicked(MouseEvent e) {}
377    @Override
378    public void mouseEntered(MouseEvent e) {}
379    @Override
380    public void mouseExited(MouseEvent e) {}
381    @Override
382    public void mouseMoved(MouseEvent e) {}
383}