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.MapViewPaintable; 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 implements MapViewPaintable { 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 130 * drawComponent. 131 */ 132 private boolean aspectRatio; 133 134 /** 135 * <code>true</code> if we should paint a lasso instead of a rectangle. 136 */ 137 private boolean lassoMode; 138 /** 139 * The polygon to store the selection outline if {@link #lassoMode} is used. 140 */ 141 private Polygon lasso = new Polygon(); 142 143 /** 144 * The result of the last selection. 145 */ 146 private Polygon selectionResult = new Polygon(); 147 148 private final SelectionHintLayer selectionHintLayer = new SelectionHintLayer(); 149 150 /** 151 * Create a new SelectionManager. 152 * 153 * @param selectionEndedListener The action listener that receives the event when 154 * the left button is released. 155 * @param aspectRatio If true, the selection window must obtain the aspect 156 * ratio of the drawComponent. 157 * @param navComp The component that provides us with OSM data and the aspect is taken from. 158 */ 159 public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) { 160 this.selectionEndedListener = selectionEndedListener; 161 this.aspectRatio = aspectRatio; 162 this.nc = navComp; 163 } 164 165 /** 166 * Register itself at the given event source and add a hint layer. 167 * @param eventSource The emitter of the mouse events. 168 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 169 */ 170 public void register(MapView eventSource, boolean lassoMode) { 171 this.lassoMode = lassoMode; 172 eventSource.addMouseListener(this); 173 eventSource.addMouseMotionListener(this); 174 selectionEndedListener.addPropertyChangeListener(this); 175 eventSource.addPropertyChangeListener("scale", new PropertyChangeListener() { 176 @Override 177 public void propertyChange(PropertyChangeEvent evt) { 178 abortSelecting(); 179 } 180 }); 181 eventSource.addTemporaryLayer(selectionHintLayer); 182 } 183 184 /** 185 * Unregister itself from the given event source and hide the selection hint layer. 186 * 187 * @param eventSource The emitter of the mouse events. 188 */ 189 public void unregister(MapView eventSource) { 190 abortSelecting(); 191 eventSource.removeTemporaryLayer(selectionHintLayer); 192 eventSource.removeMouseListener(this); 193 eventSource.removeMouseMotionListener(this); 194 selectionEndedListener.removePropertyChangeListener(this); 195 } 196 197 /** 198 * If the correct button, from the "drawing rectangle" mode 199 */ 200 @Override 201 public void mousePressed(MouseEvent e) { 202 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && Main.main.getCurrentDataSet() != null) { 203 SelectByInternalPointAction.performSelection(Main.map.mapView.getEastNorth(e.getX(), e.getY()), 204 (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) > 0, 205 (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0); 206 } else if (e.getButton() == MouseEvent.BUTTON1) { 207 mousePosStart = mousePos = e.getPoint(); 208 209 lasso.reset(); 210 lasso.addPoint(mousePosStart.x, mousePosStart.y); 211 } 212 } 213 214 /** 215 * If the correct button is hold, draw the rectangle. 216 */ 217 @Override 218 public void mouseDragged(MouseEvent e) { 219 int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK); 220 221 if (buttonPressed != 0) { 222 if (mousePosStart == null) { 223 mousePosStart = mousePos = e.getPoint(); 224 } 225 selectionAreaChanged(); 226 } 227 228 if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) { 229 mousePos = e.getPoint(); 230 addLassoPoint(e.getPoint()); 231 selectionAreaChanged(); 232 } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) { 233 moveSelection(e.getX()-mousePos.x, e.getY()-mousePos.y); 234 mousePos = e.getPoint(); 235 selectionAreaChanged(); 236 } 237 } 238 239 /** 240 * Moves the current selection by some pixels. 241 * @param dx How much to move it in x direction. 242 * @param dy How much to move it in y direction. 243 */ 244 private void moveSelection(int dx, int dy) { 245 mousePosStart.x += dx; 246 mousePosStart.y += dy; 247 lasso.translate(dx, dy); 248 } 249 250 /** 251 * Check the state of the keys and buttons and set the selection accordingly. 252 */ 253 @Override 254 public void mouseReleased(MouseEvent e) { 255 if (e.getButton() == MouseEvent.BUTTON1) { 256 endSelecting(e); 257 } 258 } 259 260 /** 261 * Ends the selection of the current area. This simulates a release of mouse button 1. 262 * @param e A mouse event that caused this. Needed for backward compatibility. 263 */ 264 public void endSelecting(MouseEvent e) { 265 mousePos = e.getPoint(); 266 if (lassoMode) { 267 addLassoPoint(e.getPoint()); 268 } 269 270 // Left mouse was released while right is still pressed. 271 boolean rightMouseStillPressed = (e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0; 272 273 if (!rightMouseStillPressed) { 274 selectingDone(e); 275 } 276 abortSelecting(); 277 } 278 279 private void addLassoPoint(Point point) { 280 if (isNoSelection()) { 281 return; 282 } 283 lasso.addPoint(point.x, point.y); 284 } 285 286 private boolean isNoSelection() { 287 return mousePos == null || mousePosStart == null || mousePos == mousePosStart; 288 } 289 290 /** 291 * Calculate and return the current selection rectangle 292 * @return A rectangle that spans from mousePos to mouseStartPos 293 */ 294 private Rectangle getSelectionRectangle() { 295 int x = mousePosStart.x; 296 int y = mousePosStart.y; 297 int w = mousePos.x - mousePosStart.x; 298 int h = mousePos.y - mousePosStart.y; 299 if (w < 0) { 300 x += w; 301 w = -w; 302 } 303 if (h < 0) { 304 y += h; 305 h = -h; 306 } 307 308 if (aspectRatio) { 309 /* Keep the aspect ratio by growing the rectangle; the 310 * rectangle is always under the cursor. */ 311 double aspectRatio = (double) nc.getWidth()/nc.getHeight(); 312 if ((double) w/h < aspectRatio) { 313 int neww = (int) (h*aspectRatio); 314 if (mousePos.x < mousePosStart.x) { 315 x += w - neww; 316 } 317 w = neww; 318 } else { 319 int newh = (int) (w/aspectRatio); 320 if (mousePos.y < mousePosStart.y) { 321 y += h - newh; 322 } 323 h = newh; 324 } 325 } 326 327 return new Rectangle(x, y, w, h); 328 } 329 330 /** 331 * If the action goes inactive, remove the selection rectangle from screen 332 */ 333 @Override 334 public void propertyChange(PropertyChangeEvent evt) { 335 if ("active".equals(evt.getPropertyName()) && !(Boolean) evt.getNewValue()) { 336 abortSelecting(); 337 } 338 } 339 340 /** 341 * Stores the current selection and stores the result in {@link #selectionResult} to be retrieved by 342 * {@link #getSelectedObjects(boolean)} later. 343 * @param e The mouse event that caused the selection to be finished. 344 */ 345 private void selectingDone(MouseEvent e) { 346 if (isNoSelection()) { 347 // Nothing selected. 348 return; 349 } 350 Rectangle r; 351 if (lassoMode) { 352 r = lasso.getBounds(); 353 354 selectionResult = new Polygon(lasso.xpoints, lasso.ypoints, lasso.npoints); 355 } else { 356 r = getSelectionRectangle(); 357 358 selectionResult = rectToPolygon(r); 359 } 360 selectionEndedListener.selectionEnded(r, e); 361 } 362 363 private void abortSelecting() { 364 if (mousePosStart != null) { 365 mousePos = mousePosStart = null; 366 lasso.reset(); 367 selectionAreaChanged(); 368 } 369 } 370 371 private static void selectionAreaChanged() { 372 // Trigger a redraw of the map view. 373 // A nicer way would be to provide change events for the temporary layer. 374 Main.map.mapView.repaint(); 375 } 376 377 /** 378 * Return a list of all objects in the active/last selection, respecting the different 379 * modifier. 380 * 381 * @param alt Whether the alt key was pressed, which means select all 382 * objects that are touched, instead those which are completely covered. 383 * @return The collection of selected objects. 384 */ 385 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) { 386 387 Collection<OsmPrimitive> selection = new LinkedList<>(); 388 389 // whether user only clicked, not dragged. 390 boolean clicked = false; 391 Rectangle bounding = selectionResult.getBounds(); 392 if (bounding.height <= 2 && bounding.width <= 2) { 393 clicked = true; 394 } 395 396 if (clicked) { 397 Point center = new Point(selectionResult.xpoints[0], selectionResult.ypoints[0]); 398 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false); 399 if (osm != null) { 400 selection.add(osm); 401 } 402 } else { 403 // nodes 404 for (Node n : nc.getCurrentDataSet().getNodes()) { 405 if (n.isSelectable() && selectionResult.contains(nc.getPoint2D(n))) { 406 selection.add(n); 407 } 408 } 409 410 // ways 411 for (Way w : nc.getCurrentDataSet().getWays()) { 412 if (!w.isSelectable() || w.getNodesCount() == 0) { 413 continue; 414 } 415 if (alt) { 416 for (Node n : w.getNodes()) { 417 if (!n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n))) { 418 selection.add(w); 419 break; 420 } 421 } 422 } else { 423 boolean allIn = true; 424 for (Node n : w.getNodes()) { 425 if (!n.isIncomplete() && !selectionResult.contains(nc.getPoint(n))) { 426 allIn = false; 427 break; 428 } 429 } 430 if (allIn) { 431 selection.add(w); 432 } 433 } 434 } 435 } 436 return selection; 437 } 438 439 private static Polygon rectToPolygon(Rectangle r) { 440 Polygon poly = new Polygon(); 441 442 poly.addPoint(r.x, r.y); 443 poly.addPoint(r.x, r.y + r.height); 444 poly.addPoint(r.x + r.width, r.y + r.height); 445 poly.addPoint(r.x + r.width, r.y); 446 447 return poly; 448 } 449 450 /** 451 * Enables or disables the lasso mode. 452 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 453 */ 454 public void setLassoMode(boolean lassoMode) { 455 this.lassoMode = lassoMode; 456 } 457 458 @Override 459 public void mouseClicked(MouseEvent e) {} 460 461 @Override 462 public void mouseEntered(MouseEvent e) {} 463 464 @Override 465 public void mouseExited(MouseEvent e) {} 466 467 @Override 468 public void mouseMoved(MouseEvent e) {} 469}