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.Utils; 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 = Utils.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.getNodesCount() == 0) { 394 continue; 395 } 396 if (alt) { 397 for (Node n : w.getNodes()) { 398 if (!n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n))) { 399 selection.add(w); 400 break; 401 } 402 } 403 } else { 404 boolean allIn = true; 405 for (Node n : w.getNodes()) { 406 if (!n.isIncomplete() && !selectionResult.contains(nc.getPoint(n))) { 407 allIn = false; 408 break; 409 } 410 } 411 if (allIn) { 412 selection.add(w); 413 } 414 } 415 } 416 } 417 return selection; 418 } 419 420 private static Polygon rectToPolygon(Rectangle r) { 421 Polygon poly = new Polygon(); 422 423 poly.addPoint(r.x, r.y); 424 poly.addPoint(r.x, r.y + r.height); 425 poly.addPoint(r.x + r.width, r.y + r.height); 426 poly.addPoint(r.x + r.width, r.y); 427 428 return poly; 429 } 430 431 /** 432 * Enables or disables the lasso mode. 433 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 434 */ 435 public void setLassoMode(boolean lassoMode) { 436 this.lassoMode = lassoMode; 437 } 438 439 @Override 440 public void mouseClicked(MouseEvent e) { 441 // Do nothing 442 } 443 444 @Override 445 public void mouseEntered(MouseEvent e) { 446 // Do nothing 447 } 448 449 @Override 450 public void mouseExited(MouseEvent e) { 451 // Do nothing 452 } 453 454 @Override 455 public void mouseMoved(MouseEvent e) { 456 // Do nothing 457 } 458}