source: josm/trunk/src/org/openstreetmap/josm/gui/SelectionManager.java

Last change on this file was 19167, checked in by taylor.smock, 11 months ago

Fix #23830: Slow selection when there is a lot of data

We were previously iterating through all nodes and ways when making a selection.
We don't have to do that since we have a way to efficiently find objects in a
bbox.

  • Property svn:eol-style set to native
File size: 15.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import java.awt.Color;
5import java.awt.Graphics2D;
6import java.awt.Point;
7import java.awt.Polygon;
8import java.awt.Rectangle;
9import java.awt.event.InputEvent;
10import java.awt.event.MouseEvent;
11import java.awt.event.MouseListener;
12import java.awt.event.MouseMotionListener;
13import java.beans.PropertyChangeEvent;
14import java.beans.PropertyChangeListener;
15import java.util.Collection;
16import java.util.LinkedList;
17
18import javax.swing.Action;
19
20import org.openstreetmap.josm.actions.SelectByInternalPointAction;
21import org.openstreetmap.josm.data.Bounds;
22import org.openstreetmap.josm.data.osm.BBox;
23import org.openstreetmap.josm.data.osm.DataSet;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Way;
27import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
28import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
29import org.openstreetmap.josm.tools.ColorHelper;
30import org.openstreetmap.josm.tools.PlatformManager;
31
32/**
33 * Manages the selection of a rectangle or a lasso loop. Listening to left and right mouse button
34 * presses and to mouse motions and draw the rectangle accordingly.
35 *
36 * Left mouse button selects a rectangle from the press until release. Pressing
37 * right mouse button while left is still pressed enable the selection area to move
38 * around. Releasing the left button fires an action event to the listener given
39 * at constructor, except if the right is still pressed, which just remove the
40 * selection rectangle and does nothing.
41 *
42 * It is possible to switch between lasso selection and rectangle selection by using {@link #setLassoMode(boolean)}.
43 *
44 * The point where the left mouse button was pressed and the current mouse
45 * position are two opposite corners of the selection rectangle.
46 *
47 * For rectangle mode, it is possible to specify an aspect ratio (width per height) which the
48 * selection rectangle always must have. In this case, the selection rectangle
49 * will be the largest window with this aspect ratio, where the position the left
50 * mouse button was pressed and the corner of the current mouse position are at
51 * opposite sites (the mouse position corner is the corner nearest to the mouse
52 * cursor).
53 *
54 * When the left mouse button was released, an ActionEvent is send to the
55 * ActionListener given at constructor. The source of this event is this manager.
56 *
57 * @author imi
58 */
59public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener {
60
61 /**
62 * This is the interface that an user of SelectionManager has to implement
63 * to get informed when a selection closes.
64 * @author imi
65 */
66 public interface SelectionEnded extends Action {
67 /**
68 * Called, when the left mouse button was released.
69 * @param r The rectangle that encloses the current selection.
70 * @param e The mouse event.
71 * @see InputEvent#getModifiersEx()
72 * @see SelectionManager#getSelectedObjects(boolean)
73 */
74 void selectionEnded(Rectangle r, MouseEvent e);
75 }
76
77 /**
78 * This draws the selection hint (rectangle or lasso polygon) on the screen.
79 *
80 * @author Michael Zangl
81 */
82 private final class SelectionHintLayer extends AbstractMapViewPaintable {
83 @Override
84 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
85 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
86 return;
87 Color color = ColorHelper.complement(PaintColors.getBackgroundColor());
88 g.setColor(color);
89 if (lassoMode) {
90 g.drawPolygon(lasso);
91
92 g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() / 8));
93 g.fillPolygon(lasso);
94 } else {
95 Rectangle paintRect = getSelectionRectangle();
96 g.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
97 }
98 }
99 }
100
101 /**
102 * The listener that receives the events after left mouse button is released.
103 */
104 private final SelectionEnded selectionEndedListener;
105 /**
106 * Position of the map when the mouse button was pressed.
107 * If this is not <code>null</code>, a rectangle/lasso line is drawn on screen.
108 * If this is <code>null</code>, no selection is active.
109 */
110 private Point mousePosStart;
111 /**
112 * The last position of the mouse while the mouse button was pressed.
113 */
114 private Point mousePos;
115 /**
116 * The Component that provides us with OSM data and the aspect is taken from.
117 */
118 private final NavigatableComponent nc;
119 /**
120 * Whether the selection rectangle must obtain the aspect ratio of the drawComponent.
121 */
122 private final boolean aspectRatio;
123
124 /**
125 * <code>true</code> if we should paint a lasso instead of a rectangle.
126 */
127 private boolean lassoMode;
128 /**
129 * The polygon to store the selection outline if {@link #lassoMode} is used.
130 */
131 private final Polygon lasso = new Polygon();
132
133 /**
134 * The result of the last selection.
135 */
136 private Polygon selectionResult = new Polygon();
137
138 private final SelectionHintLayer selectionHintLayer = new SelectionHintLayer();
139
140 /**
141 * Create a new SelectionManager.
142 *
143 * @param selectionEndedListener The action listener that receives the event when
144 * the left button is released.
145 * @param aspectRatio If true, the selection window must obtain the aspect
146 * ratio of the drawComponent.
147 * @param navComp The component that provides us with OSM data and the aspect is taken from.
148 */
149 public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
150 this.selectionEndedListener = selectionEndedListener;
151 this.aspectRatio = aspectRatio;
152 this.nc = navComp;
153 }
154
155 /**
156 * Register itself at the given event source and add a hint layer.
157 * @param eventSource The emitter of the mouse events.
158 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
159 */
160 public void register(MapView eventSource, boolean lassoMode) {
161 this.lassoMode = lassoMode;
162 eventSource.addMouseListener(this);
163 eventSource.addMouseMotionListener(this);
164 selectionEndedListener.addPropertyChangeListener(this);
165 eventSource.addPropertyChangeListener("scale", evt -> abortSelecting());
166 eventSource.addTemporaryLayer(selectionHintLayer);
167 }
168
169 /**
170 * Unregister itself from the given event source and hide the selection hint layer.
171 *
172 * @param eventSource The emitter of the mouse events.
173 */
174 public void unregister(MapView eventSource) {
175 abortSelecting();
176 eventSource.removeTemporaryLayer(selectionHintLayer);
177 eventSource.removeMouseListener(this);
178 eventSource.removeMouseMotionListener(this);
179 selectionEndedListener.removePropertyChangeListener(this);
180 }
181
182 /**
183 * If the correct button, from the "drawing rectangle" mode
184 */
185 @Override
186 public void mousePressed(MouseEvent e) {
187 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && MainApplication.getLayerManager().getActiveDataSet() != null) {
188 SelectByInternalPointAction.performSelection(MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY()),
189 (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0,
190 (e.getModifiersEx() & PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()) != 0);
191 } else if (e.getButton() == MouseEvent.BUTTON1) {
192 mousePosStart = mousePos = e.getPoint();
193
194 lasso.reset();
195 lasso.addPoint(mousePosStart.x, mousePosStart.y);
196 }
197 }
198
199 /**
200 * If the correct button is hold, draw the rectangle.
201 */
202 @Override
203 public void mouseDragged(MouseEvent e) {
204 int buttonPressed = e.getModifiersEx() & (InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK);
205
206 if (buttonPressed != 0) {
207 if (mousePosStart == null) {
208 mousePosStart = mousePos = e.getPoint();
209 }
210 selectionAreaChanged();
211 }
212
213 if (buttonPressed == InputEvent.BUTTON1_DOWN_MASK) {
214 mousePos = e.getPoint();
215 addLassoPoint(e.getPoint());
216 selectionAreaChanged();
217 } else if (buttonPressed == (InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK)) {
218 moveSelection(e.getX()-mousePos.x, e.getY()-mousePos.y);
219 mousePos = e.getPoint();
220 selectionAreaChanged();
221 }
222 }
223
224 /**
225 * Moves the current selection by some pixels.
226 * @param dx How much to move it in x direction.
227 * @param dy How much to move it in y direction.
228 */
229 private void moveSelection(int dx, int dy) {
230 mousePosStart.x += dx;
231 mousePosStart.y += dy;
232 lasso.translate(dx, dy);
233 }
234
235 /**
236 * Check the state of the keys and buttons and set the selection accordingly.
237 */
238 @Override
239 public void mouseReleased(MouseEvent e) {
240 if (e.getButton() == MouseEvent.BUTTON1) {
241 endSelecting(e);
242 }
243 }
244
245 /**
246 * Ends the selection of the current area. This simulates a release of mouse button 1.
247 * @param e A mouse event that caused this. Needed for backward compatibility.
248 */
249 public void endSelecting(MouseEvent e) {
250 mousePos = e.getPoint();
251 if (lassoMode) {
252 addLassoPoint(e.getPoint());
253 }
254
255 // Left mouse was released while right is still pressed.
256 boolean rightMouseStillPressed = (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0;
257
258 if (!rightMouseStillPressed) {
259 selectingDone(e);
260 }
261 abortSelecting();
262 }
263
264 private void addLassoPoint(Point point) {
265 if (isNoSelection()) {
266 return;
267 }
268 lasso.addPoint(point.x, point.y);
269 }
270
271 private boolean isNoSelection() {
272 return mousePos == null || mousePosStart == null || mousePos == mousePosStart;
273 }
274
275 /**
276 * Calculate and return the current selection rectangle
277 * @return A rectangle that spans from mousePos to mouseStartPos
278 */
279 private Rectangle getSelectionRectangle() {
280 int x = mousePosStart.x;
281 int y = mousePosStart.y;
282 int w = mousePos.x - mousePosStart.x;
283 int h = mousePos.y - mousePosStart.y;
284 if (w < 0) {
285 x += w;
286 w = -w;
287 }
288 if (h < 0) {
289 y += h;
290 h = -h;
291 }
292
293 if (aspectRatio) {
294 /* Keep the aspect ratio by growing the rectangle; the
295 * rectangle is always under the cursor. */
296 double aspectRatio = (double) nc.getWidth()/nc.getHeight();
297 if ((double) w/h < aspectRatio) {
298 int neww = (int) (h*aspectRatio);
299 if (mousePos.x < mousePosStart.x) {
300 x += w - neww;
301 }
302 w = neww;
303 } else {
304 int newh = (int) (w/aspectRatio);
305 if (mousePos.y < mousePosStart.y) {
306 y += h - newh;
307 }
308 h = newh;
309 }
310 }
311
312 return new Rectangle(x, y, w, h);
313 }
314
315 /**
316 * If the action goes inactive, remove the selection rectangle from screen
317 */
318 @Override
319 public void propertyChange(PropertyChangeEvent evt) {
320 if ("active".equals(evt.getPropertyName()) && Boolean.FALSE.equals(evt.getNewValue())) {
321 abortSelecting();
322 }
323 }
324
325 /**
326 * Stores the current selection and stores the result in {@link #selectionResult} to be retrieved by
327 * {@link #getSelectedObjects(boolean)} later.
328 * @param e The mouse event that caused the selection to be finished.
329 */
330 private void selectingDone(MouseEvent e) {
331 if (isNoSelection()) {
332 // Nothing selected.
333 return;
334 }
335 Rectangle r;
336 if (lassoMode) {
337 r = lasso.getBounds();
338
339 selectionResult = new Polygon(lasso.xpoints, lasso.ypoints, lasso.npoints);
340 } else {
341 r = getSelectionRectangle();
342
343 selectionResult = rectToPolygon(r);
344 }
345 selectionEndedListener.selectionEnded(r, e);
346 }
347
348 private void abortSelecting() {
349 if (mousePosStart != null) {
350 mousePos = mousePosStart = null;
351 lasso.reset();
352 selectionAreaChanged();
353 }
354 }
355
356 private void selectionAreaChanged() {
357 selectionHintLayer.invalidate();
358 }
359
360 /**
361 * Return a list of all objects in the active/last selection, respecting the different
362 * modifier.
363 *
364 * @param alt Whether the alt key was pressed, which means select all
365 * objects that are touched, instead those which are completely covered.
366 * @return The collection of selected objects.
367 */
368 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
369 Collection<OsmPrimitive> selection = new LinkedList<>();
370
371 // whether user only clicked, not dragged.
372 boolean clicked = false;
373 Rectangle bounding = selectionResult.getBounds();
374 if (bounding.height <= 2 && bounding.width <= 2) {
375 clicked = true;
376 }
377
378 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
379 if (clicked) {
380 Point center = new Point(selectionResult.xpoints[0], selectionResult.ypoints[0]);
381 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive::isSelectable, false);
382 if (osm != null) {
383 selection.add(osm);
384 }
385 } else if (ds != null) {
386 final BBox bbox = nc.getLatLonBounds(bounding).toBBox();
387 // nodes
388 for (Node n : ds.searchNodes(bbox)) {
389 if (n.isSelectable() && selectionResult.contains(nc.getPoint2D(n))) {
390 selection.add(n);
391 }
392 }
393
394 // ways
395 for (Way w : ds.searchWays(bbox)) {
396 if (!w.isSelectable() || w.isEmpty()) {
397 continue;
398 }
399 if (alt) {
400 if (w.getNodes().stream().anyMatch(n -> !n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n)))) {
401 selection.add(w);
402 }
403 } else {
404 boolean allIn = w.getNodes().stream().allMatch(n -> n.isIncomplete() || selectionResult.contains(nc.getPoint(n)));
405 if (allIn) {
406 selection.add(w);
407 }
408 }
409 }
410 }
411 return selection;
412 }
413
414 private static Polygon rectToPolygon(Rectangle r) {
415 Polygon poly = new Polygon();
416
417 poly.addPoint(r.x, r.y);
418 poly.addPoint(r.x, r.y + r.height);
419 poly.addPoint(r.x + r.width, r.y + r.height);
420 poly.addPoint(r.x + r.width, r.y);
421
422 return poly;
423 }
424
425 /**
426 * Enables or disables the lasso mode.
427 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
428 */
429 public void setLassoMode(boolean lassoMode) {
430 this.lassoMode = lassoMode;
431 }
432
433 @Override
434 public void mouseClicked(MouseEvent e) {
435 // Do nothing
436 }
437
438 @Override
439 public void mouseEntered(MouseEvent e) {
440 // Do nothing
441 }
442
443 @Override
444 public void mouseExited(MouseEvent e) {
445 // Do nothing
446 }
447
448 @Override
449 public void mouseMoved(MouseEvent e) {
450 // Do nothing
451 }
452}
Note: See TracBrowser for help on using the repository browser.