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