001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import java.awt.Color;
005import java.awt.Graphics2D;
006import java.awt.Point;
007import java.awt.Polygon;
008import java.awt.Rectangle;
009import java.awt.event.InputEvent;
010import java.awt.event.MouseEvent;
011import java.awt.event.MouseListener;
012import java.awt.event.MouseMotionListener;
013import java.beans.PropertyChangeEvent;
014import java.beans.PropertyChangeListener;
015import java.util.Collection;
016import java.util.LinkedList;
017
018import javax.swing.Action;
019
020import org.openstreetmap.josm.actions.SelectByInternalPointAction;
021import org.openstreetmap.josm.data.Bounds;
022import org.openstreetmap.josm.data.osm.DataSet;
023import org.openstreetmap.josm.data.osm.Node;
024import org.openstreetmap.josm.data.osm.OsmPrimitive;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
027import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
028import org.openstreetmap.josm.tools.ColorHelper;
029
030/**
031 * Manages the selection of a rectangle or a lasso loop. Listening to left and right mouse button
032 * presses and to mouse motions and draw the rectangle accordingly.
033 *
034 * Left mouse button selects a rectangle from the press until release. Pressing
035 * right mouse button while left is still pressed enable the selection area to move
036 * around. Releasing the left button fires an action event to the listener given
037 * at constructor, except if the right is still pressed, which just remove the
038 * selection rectangle and does nothing.
039 *
040 * It is possible to switch between lasso selection and rectangle selection by using {@link #setLassoMode(boolean)}.
041 *
042 * The point where the left mouse button was pressed and the current mouse
043 * position are two opposite corners of the selection rectangle.
044 *
045 * For rectangle mode, it is possible to specify an aspect ratio (width per height) which the
046 * selection rectangle always must have. In this case, the selection rectangle
047 * will be the largest window with this aspect ratio, where the position the left
048 * mouse button was pressed and the corner of the current mouse position are at
049 * opposite sites (the mouse position corner is the corner nearest to the mouse
050 * cursor).
051 *
052 * When the left mouse button was released, an ActionEvent is send to the
053 * ActionListener given at constructor. The source of this event is this manager.
054 *
055 * @author imi
056 */
057public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener {
058
059    /**
060     * This is the interface that an user of SelectionManager has to implement
061     * to get informed when a selection closes.
062     * @author imi
063     */
064    public interface SelectionEnded extends Action {
065        /**
066         * Called, when the left mouse button was released.
067         * @param r The rectangle that encloses the current selection.
068         * @param e The mouse event.
069         * @see InputEvent#getModifiersEx()
070         * @see SelectionManager#getSelectedObjects(boolean)
071         */
072        void selectionEnded(Rectangle r, MouseEvent e);
073    }
074
075    /**
076     * This draws the selection hint (rectangle or lasso polygon) on the screen.
077     *
078     * @author Michael Zangl
079     */
080    private class SelectionHintLayer extends AbstractMapViewPaintable {
081        @Override
082        public void paint(Graphics2D g, MapView mv, Bounds bbox) {
083            if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
084                return;
085            Color color = ColorHelper.complement(PaintColors.getBackgroundColor());
086            g.setColor(color);
087            if (lassoMode) {
088                g.drawPolygon(lasso);
089
090                g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() / 8));
091                g.fillPolygon(lasso);
092            } else {
093                Rectangle paintRect = getSelectionRectangle();
094                g.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
095            }
096        }
097    }
098
099    /**
100     * The listener that receives the events after left mouse button is released.
101     */
102    private final SelectionEnded selectionEndedListener;
103    /**
104     * Position of the map when the mouse button was pressed.
105     * If this is not <code>null</code>, a rectangle/lasso line is drawn on screen.
106     * If this is <code>null</code>, no selection is active.
107     */
108    private Point mousePosStart;
109    /**
110     * The last position of the mouse while the mouse button was pressed.
111     */
112    private Point mousePos;
113    /**
114     * The Component that provides us with OSM data and the aspect is taken from.
115     */
116    private final NavigatableComponent nc;
117    /**
118     * Whether the selection rectangle must obtain the aspect ratio of the drawComponent.
119     */
120    private final boolean aspectRatio;
121
122    /**
123     * <code>true</code> if we should paint a lasso instead of a rectangle.
124     */
125    private boolean lassoMode;
126    /**
127     * The polygon to store the selection outline if {@link #lassoMode} is used.
128     */
129    private final Polygon lasso = new Polygon();
130
131    /**
132     * The result of the last selection.
133     */
134    private Polygon selectionResult = new Polygon();
135
136    private final SelectionHintLayer selectionHintLayer = new SelectionHintLayer();
137
138    /**
139     * Create a new SelectionManager.
140     *
141     * @param selectionEndedListener The action listener that receives the event when
142     *      the left button is released.
143     * @param aspectRatio If true, the selection window must obtain the aspect
144     *      ratio of the drawComponent.
145     * @param navComp The component that provides us with OSM data and the aspect is taken from.
146     */
147    public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
148        this.selectionEndedListener = selectionEndedListener;
149        this.aspectRatio = aspectRatio;
150        this.nc = navComp;
151    }
152
153    /**
154     * Register itself at the given event source and add a hint layer.
155     * @param eventSource The emitter of the mouse events.
156     * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
157     */
158    public void register(MapView eventSource, boolean lassoMode) {
159       this.lassoMode = lassoMode;
160        eventSource.addMouseListener(this);
161        eventSource.addMouseMotionListener(this);
162        selectionEndedListener.addPropertyChangeListener(this);
163        eventSource.addPropertyChangeListener("scale", evt -> abortSelecting());
164        eventSource.addTemporaryLayer(selectionHintLayer);
165    }
166
167    /**
168     * Unregister itself from the given event source and hide the selection hint layer.
169     *
170     * @param eventSource The emitter of the mouse events.
171     */
172    public void unregister(MapView eventSource) {
173        abortSelecting();
174        eventSource.removeTemporaryLayer(selectionHintLayer);
175        eventSource.removeMouseListener(this);
176        eventSource.removeMouseMotionListener(this);
177        selectionEndedListener.removePropertyChangeListener(this);
178    }
179
180    /**
181     * If the correct button, from the "drawing rectangle" mode
182     */
183    @Override
184    public void mousePressed(MouseEvent e) {
185        if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && MainApplication.getLayerManager().getActiveDataSet() != null) {
186            SelectByInternalPointAction.performSelection(MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY()),
187                    (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0,
188                    (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0);
189        } else if (e.getButton() == MouseEvent.BUTTON1) {
190            mousePosStart = mousePos = e.getPoint();
191
192            lasso.reset();
193            lasso.addPoint(mousePosStart.x, mousePosStart.y);
194        }
195    }
196
197    /**
198     * If the correct button is hold, draw the rectangle.
199     */
200    @Override
201    public void mouseDragged(MouseEvent e) {
202        int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK);
203
204        if (buttonPressed != 0) {
205            if (mousePosStart == null) {
206                mousePosStart = mousePos = e.getPoint();
207            }
208            selectionAreaChanged();
209        }
210
211        if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) {
212            mousePos = e.getPoint();
213            addLassoPoint(e.getPoint());
214            selectionAreaChanged();
215        } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) {
216            moveSelection(e.getX()-mousePos.x, e.getY()-mousePos.y);
217            mousePos = e.getPoint();
218            selectionAreaChanged();
219        }
220    }
221
222    /**
223     * Moves the current selection by some pixels.
224     * @param dx How much to move it in x direction.
225     * @param dy How much to move it in y direction.
226     */
227    private void moveSelection(int dx, int dy) {
228        mousePosStart.x += dx;
229        mousePosStart.y += dy;
230        lasso.translate(dx, dy);
231    }
232
233    /**
234     * Check the state of the keys and buttons and set the selection accordingly.
235     */
236    @Override
237    public void mouseReleased(MouseEvent e) {
238        if (e.getButton() == MouseEvent.BUTTON1) {
239            endSelecting(e);
240        }
241    }
242
243    /**
244     * Ends the selection of the current area. This simulates a release of mouse button 1.
245     * @param e A mouse event that caused this. Needed for backward compatibility.
246     */
247    public void endSelecting(MouseEvent e) {
248        mousePos = e.getPoint();
249        if (lassoMode) {
250            addLassoPoint(e.getPoint());
251        }
252
253        // Left mouse was released while right is still pressed.
254        boolean rightMouseStillPressed = (e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0;
255
256        if (!rightMouseStillPressed) {
257            selectingDone(e);
258        }
259        abortSelecting();
260    }
261
262    private void addLassoPoint(Point point) {
263        if (isNoSelection()) {
264            return;
265        }
266        lasso.addPoint(point.x, point.y);
267    }
268
269    private boolean isNoSelection() {
270        return mousePos == null || mousePosStart == null || mousePos == mousePosStart;
271    }
272
273    /**
274     * Calculate and return the current selection rectangle
275     * @return A rectangle that spans from mousePos to mouseStartPos
276     */
277    private Rectangle getSelectionRectangle() {
278        int x = mousePosStart.x;
279        int y = mousePosStart.y;
280        int w = mousePos.x - mousePosStart.x;
281        int h = mousePos.y - mousePosStart.y;
282        if (w < 0) {
283            x += w;
284            w = -w;
285        }
286        if (h < 0) {
287            y += h;
288            h = -h;
289        }
290
291        if (aspectRatio) {
292            /* Keep the aspect ratio by growing the rectangle; the
293             * rectangle is always under the cursor. */
294            double aspectRatio = (double) nc.getWidth()/nc.getHeight();
295            if ((double) w/h < aspectRatio) {
296                int neww = (int) (h*aspectRatio);
297                if (mousePos.x < mousePosStart.x) {
298                    x += w - neww;
299                }
300                w = neww;
301            } else {
302                int newh = (int) (w/aspectRatio);
303                if (mousePos.y < mousePosStart.y) {
304                    y += h - newh;
305                }
306                h = newh;
307            }
308        }
309
310        return new Rectangle(x, y, w, h);
311    }
312
313    /**
314     * If the action goes inactive, remove the selection rectangle from screen
315     */
316    @Override
317    public void propertyChange(PropertyChangeEvent evt) {
318        if ("active".equals(evt.getPropertyName()) && !(Boolean) evt.getNewValue()) {
319            abortSelecting();
320        }
321    }
322
323    /**
324     * Stores the  current selection and stores the result in {@link #selectionResult} to  be retrieved by
325     * {@link #getSelectedObjects(boolean)} later.
326     * @param e The mouse event that caused the selection to be finished.
327     */
328    private void selectingDone(MouseEvent e) {
329        if (isNoSelection()) {
330            // Nothing selected.
331            return;
332        }
333        Rectangle r;
334        if (lassoMode) {
335            r = lasso.getBounds();
336
337            selectionResult = new Polygon(lasso.xpoints, lasso.ypoints, lasso.npoints);
338        } else {
339            r = getSelectionRectangle();
340
341            selectionResult = rectToPolygon(r);
342        }
343        selectionEndedListener.selectionEnded(r, e);
344    }
345
346    private void abortSelecting() {
347        if (mousePosStart != null) {
348            mousePos = mousePosStart = null;
349            lasso.reset();
350            selectionAreaChanged();
351        }
352    }
353
354    private void selectionAreaChanged() {
355        selectionHintLayer.invalidate();
356    }
357
358    /**
359     * Return a list of all objects in the active/last selection, respecting the different
360     * modifier.
361     *
362     * @param alt Whether the alt key was pressed, which means select all
363     * objects that are touched, instead those which are completely covered.
364     * @return The collection of selected objects.
365     */
366    public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
367        Collection<OsmPrimitive> selection = new LinkedList<>();
368
369        // whether user only clicked, not dragged.
370        boolean clicked = false;
371        Rectangle bounding = selectionResult.getBounds();
372        if (bounding.height <= 2 && bounding.width <= 2) {
373            clicked = true;
374        }
375
376        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
377        if (clicked) {
378            Point center = new Point(selectionResult.xpoints[0], selectionResult.ypoints[0]);
379            OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive::isSelectable, false);
380            if (osm != null) {
381                selection.add(osm);
382            }
383        } else if (ds != null) {
384            // nodes
385            for (Node n : ds.getNodes()) {
386                if (n.isSelectable() && selectionResult.contains(nc.getPoint2D(n))) {
387                    selection.add(n);
388                }
389            }
390
391            // ways
392            for (Way w : ds.getWays()) {
393                if (!w.isSelectable() || w.isEmpty()) {
394                    continue;
395                }
396                if (alt) {
397                    if (w.getNodes().stream().anyMatch(n -> !n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n)))) {
398                        selection.add(w);
399                    }
400                } else {
401                    boolean allIn = w.getNodes().stream().allMatch(n -> n.isIncomplete() || selectionResult.contains(nc.getPoint(n)));
402                    if (allIn) {
403                        selection.add(w);
404                    }
405                }
406            }
407        }
408        return selection;
409    }
410
411    private static Polygon rectToPolygon(Rectangle r) {
412        Polygon poly = new Polygon();
413
414        poly.addPoint(r.x, r.y);
415        poly.addPoint(r.x, r.y + r.height);
416        poly.addPoint(r.x + r.width, r.y + r.height);
417        poly.addPoint(r.x + r.width, r.y);
418
419        return poly;
420    }
421
422    /**
423     * Enables or disables the lasso mode.
424     * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
425     */
426    public void setLassoMode(boolean lassoMode) {
427        this.lassoMode = lassoMode;
428    }
429
430    @Override
431    public void mouseClicked(MouseEvent e) {
432        // Do nothing
433    }
434
435    @Override
436    public void mouseEntered(MouseEvent e) {
437        // Do nothing
438    }
439
440    @Override
441    public void mouseExited(MouseEvent e) {
442        // Do nothing
443    }
444
445    @Override
446    public void mouseMoved(MouseEvent e) {
447        // Do nothing
448    }
449}