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

Last change on this file was 18871, checked in by taylor.smock, 6 months ago

See #23218: Use newer error_prone versions when compiling on Java 11+

error_prone 2.11 dropped support for compiling with Java 8, although it still
supports compiling for Java 8. The "major" new check for us is NotJavadoc since
we used /** in quite a few places which were not javadoc.

Other "new" checks that are of interest:

  • AlreadyChecked: if (foo) { doFoo(); } else if (!foo) { doBar(); }
  • UnnecessaryStringBuilder: Avoid StringBuilder (Java converts + to StringBuilder behind-the-scenes, but may also do something else if it performs better)
  • NonApiType: Avoid specific interface types in function definitions
  • NamedLikeContextualKeyword: Avoid using restricted names for classes and methods
  • UnusedMethod: Unused private methods should be removed

This fixes most of the new error_prone issues and some SonarLint issues.

  • Property svn:eol-style set to native
File size: 67.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import java.awt.Cursor;
5import java.awt.Point;
6import java.awt.Rectangle;
7import java.awt.event.ComponentAdapter;
8import java.awt.event.ComponentEvent;
9import java.awt.event.HierarchyEvent;
10import java.awt.event.HierarchyListener;
11import java.awt.event.MouseAdapter;
12import java.awt.event.MouseEvent;
13import java.awt.geom.AffineTransform;
14import java.awt.geom.Point2D;
15import java.nio.charset.StandardCharsets;
16import java.text.NumberFormat;
17import java.util.ArrayList;
18import java.util.Collection;
19import java.util.Collections;
20import java.util.HashSet;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Map;
24import java.util.Map.Entry;
25import java.util.Objects;
26import java.util.Set;
27import java.util.Stack;
28import java.util.TreeMap;
29import java.util.concurrent.CopyOnWriteArrayList;
30import java.util.function.Predicate;
31import java.util.stream.Collectors;
32import java.util.zip.CRC32;
33
34import javax.swing.JComponent;
35import javax.swing.SwingUtilities;
36
37import org.openstreetmap.josm.data.Bounds;
38import org.openstreetmap.josm.data.ProjectionBounds;
39import org.openstreetmap.josm.data.SystemOfMeasurement;
40import org.openstreetmap.josm.data.ViewportData;
41import org.openstreetmap.josm.data.coor.EastNorth;
42import org.openstreetmap.josm.data.coor.ILatLon;
43import org.openstreetmap.josm.data.coor.LatLon;
44import org.openstreetmap.josm.data.osm.BBox;
45import org.openstreetmap.josm.data.osm.DataSet;
46import org.openstreetmap.josm.data.osm.IPrimitive;
47import org.openstreetmap.josm.data.osm.IWaySegment;
48import org.openstreetmap.josm.data.osm.Node;
49import org.openstreetmap.josm.data.osm.OsmPrimitive;
50import org.openstreetmap.josm.data.osm.Relation;
51import org.openstreetmap.josm.data.osm.Way;
52import org.openstreetmap.josm.data.osm.WaySegment;
53import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
54import org.openstreetmap.josm.data.preferences.BooleanProperty;
55import org.openstreetmap.josm.data.preferences.DoubleProperty;
56import org.openstreetmap.josm.data.preferences.IntegerProperty;
57import org.openstreetmap.josm.data.projection.Projection;
58import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
59import org.openstreetmap.josm.data.projection.ProjectionRegistry;
60import org.openstreetmap.josm.gui.PrimitiveHoverListener.PrimitiveHoverEvent;
61import org.openstreetmap.josm.gui.help.Helpful;
62import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
63import org.openstreetmap.josm.gui.layer.NativeScaleLayer.Scale;
64import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
65import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
66import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
67import org.openstreetmap.josm.gui.util.CursorManager;
68import org.openstreetmap.josm.gui.util.GuiHelper;
69import org.openstreetmap.josm.spi.preferences.Config;
70import org.openstreetmap.josm.tools.Logging;
71import org.openstreetmap.josm.tools.Utils;
72import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
73
74/**
75 * A component that can be navigated by a {@link MapMover}. Used as map view and for the
76 * zoomer in the download dialog.
77 *
78 * @author imi
79 * @since 41
80 */
81public class NavigatableComponent extends JComponent implements Helpful {
82
83 private static final double ALIGNMENT_EPSILON = 1e-3;
84
85 /**
86 * Interface to notify listeners of the change of the zoom area.
87 * @since 10600 (functional interface)
88 */
89 @FunctionalInterface
90 public interface ZoomChangeListener {
91 /**
92 * Method called when the zoom area has changed.
93 */
94 void zoomChanged();
95 }
96
97 /**
98 * To determine if a primitive is currently selectable.
99 */
100 public transient Predicate<OsmPrimitive> isSelectablePredicate = prim -> {
101 if (!prim.isSelectable()) return false;
102 // if it isn't displayed on screen, you cannot click on it
103 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
104 try {
105 return !MapPaintStyles.getStyles().get(prim, getDist100Pixel(), this).isEmpty();
106 } finally {
107 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
108 }
109 };
110
111 /** Snap distance */
112 public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
113 /** Zoom steps to get double scale */
114 public static final DoubleProperty PROP_ZOOM_RATIO = new DoubleProperty("zoom.ratio", 2.0);
115 /** Divide intervals between native resolution levels to smaller steps if they are much larger than zoom ratio */
116 public static final BooleanProperty PROP_ZOOM_INTERMEDIATE_STEPS = new BooleanProperty("zoom.intermediate-steps", true);
117 /** scale follows native resolution of layer status when layer is created */
118 public static final BooleanProperty PROP_ZOOM_SCALE_FOLLOW_NATIVE_RES_AT_LOAD = new BooleanProperty(
119 "zoom.scale-follow-native-resolution-at-load", true);
120
121 /**
122 * The layer which scale is set to.
123 */
124 private transient NativeScaleLayer nativeScaleLayer;
125
126 /**
127 * the zoom listeners
128 */
129 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();
130
131 /**
132 * Removes a zoom change listener
133 *
134 * @param listener the listener. Ignored if null or already absent
135 */
136 public static void removeZoomChangeListener(ZoomChangeListener listener) {
137 zoomChangeListeners.remove(listener);
138 }
139
140 /**
141 * Adds a zoom change listener
142 *
143 * @param listener the listener. Ignored if null or already registered.
144 */
145 public static void addZoomChangeListener(ZoomChangeListener listener) {
146 if (listener != null) {
147 zoomChangeListeners.addIfAbsent(listener);
148 }
149 }
150
151 protected static void fireZoomChanged() {
152 GuiHelper.runInEDTAndWait(() -> {
153 for (ZoomChangeListener l : zoomChangeListeners) {
154 l.zoomChanged();
155 }
156 });
157 }
158
159 private final CopyOnWriteArrayList<PrimitiveHoverListener> primitiveHoverListeners = new CopyOnWriteArrayList<>();
160 private IPrimitive previousHoveredPrimitive;
161 private final PrimitiveHoverMouseListener primitiveHoverMouseListenerHelper = new PrimitiveHoverMouseListener();
162
163 /**
164 * Removes a primitive hover listener
165 *
166 * @param listener the listener. Ignored if null or already absent.
167 * @since 18574
168 */
169 public void removePrimitiveHoverListener(PrimitiveHoverListener listener) {
170 primitiveHoverListeners.remove(listener);
171 }
172
173 /**
174 * Adds a primitive hover listener
175 *
176 * @param listener the listener. Ignored if null or already registered.
177 * @since 18574
178 */
179 public void addPrimitiveHoverListener(PrimitiveHoverListener listener) {
180 if (listener != null) {
181 primitiveHoverListeners.addIfAbsent(listener);
182 }
183 }
184
185 /**
186 * Send a {@link PrimitiveHoverEvent} to registered {@link PrimitiveHoverListener}s
187 * @param e primitive hover event information
188 * @since 18574
189 */
190 protected void firePrimitiveHovered(PrimitiveHoverEvent e) {
191 GuiHelper.runInEDT(() -> {
192 for (PrimitiveHoverListener l : primitiveHoverListeners) {
193 try {
194 l.primitiveHovered(e);
195 } catch (RuntimeException ex) {
196 Logging.logWithStackTrace(Logging.LEVEL_ERROR, "Error in primitive hover listener", ex);
197 BugReportExceptionHandler.handleException(ex);
198 }
199 }
200 });
201 }
202
203 private void updateHoveredPrimitive(IPrimitive hovered, MouseEvent e) {
204 if (!Objects.equals(hovered, previousHoveredPrimitive)) {
205 firePrimitiveHovered(new PrimitiveHoverEvent(hovered, previousHoveredPrimitive, e));
206 previousHoveredPrimitive = hovered;
207 }
208 }
209
210 // The only events that may move/resize this map view are window movements or changes to the map view size.
211 // We can clean this up more by only recalculating the state on repaint.
212 private final transient HierarchyListener hierarchyListenerNavigatableComponent = e -> {
213 long interestingFlags = HierarchyEvent.ANCESTOR_MOVED | HierarchyEvent.SHOWING_CHANGED;
214 if ((e.getChangeFlags() & interestingFlags) != 0) {
215 updateLocationState();
216 }
217 };
218
219 private final transient ComponentAdapter componentListenerNavigatableComponent = new ComponentAdapter() {
220 @Override
221 public void componentShown(ComponentEvent e) {
222 updateLocationState();
223 }
224
225 @Override
226 public void componentResized(ComponentEvent e) {
227 updateLocationState();
228 }
229 };
230
231 protected transient ViewportData initialViewport;
232
233 protected final transient CursorManager cursorManager = new CursorManager(this);
234
235 /**
236 * The current state (scale, center, ...) of this map view.
237 */
238 private transient MapViewState state;
239
240 /**
241 * Main uses weak link to store this, so we need to keep a reference.
242 */
243 private final ProjectionChangeListener projectionChangeListener = (oldValue, newValue) -> fixProjection();
244
245 /**
246 * Constructs a new {@code NavigatableComponent}.
247 */
248 public NavigatableComponent() {
249 setLayout(null);
250 state = MapViewState.createDefaultState(getWidth(), getHeight());
251 ProjectionRegistry.addProjectionChangeListener(projectionChangeListener);
252 }
253
254 @Override
255 public void addNotify() {
256 updateLocationState();
257 addHierarchyListener(hierarchyListenerNavigatableComponent);
258 addComponentListener(componentListenerNavigatableComponent);
259 addPrimitiveHoverMouseListeners();
260 super.addNotify();
261 }
262
263 @Override
264 public void removeNotify() {
265 removeHierarchyListener(hierarchyListenerNavigatableComponent);
266 removeComponentListener(componentListenerNavigatableComponent);
267 removePrimitiveHoverMouseListeners();
268 super.removeNotify();
269 }
270
271 private void addPrimitiveHoverMouseListeners() {
272 addMouseMotionListener(primitiveHoverMouseListenerHelper);
273 addMouseListener(primitiveHoverMouseListenerHelper);
274 }
275
276 private void removePrimitiveHoverMouseListeners() {
277 removeMouseMotionListener(primitiveHoverMouseListenerHelper);
278 removeMouseListener(primitiveHoverMouseListenerHelper);
279 }
280
281 /**
282 * Choose a layer that scale will be snap to its native scales.
283 * @param nativeScaleLayer layer to which scale will be snapped
284 */
285 public void setNativeScaleLayer(NativeScaleLayer nativeScaleLayer) {
286 this.nativeScaleLayer = nativeScaleLayer;
287 zoomTo(getCenter(), scaleRound(getScale()));
288 repaint();
289 }
290
291 /**
292 * Replies the layer which scale is set to.
293 * @return the current scale layer (may be {@code null})
294 */
295 public NativeScaleLayer getNativeScaleLayer() {
296 return nativeScaleLayer;
297 }
298
299 /**
300 * Get a new scale that is zoomed in from previous scale
301 * and snapped to selected native scale layer.
302 * @return new scale
303 */
304 public double scaleZoomIn() {
305 return scaleZoomManyTimes(-1);
306 }
307
308 /**
309 * Get a new scale that is zoomed out from previous scale
310 * and snapped to selected native scale layer.
311 * @return new scale
312 */
313 public double scaleZoomOut() {
314 return scaleZoomManyTimes(1);
315 }
316
317 /**
318 * Get a new scale that is zoomed in/out a number of times
319 * from previous scale and snapped to selected native scale layer.
320 * @param times count of zoom operations, negative means zoom in
321 * @return new scale
322 */
323 public double scaleZoomManyTimes(int times) {
324 if (nativeScaleLayer != null) {
325 ScaleList scaleList = nativeScaleLayer.getNativeScales();
326 if (scaleList != null) {
327 if (Boolean.TRUE.equals(PROP_ZOOM_INTERMEDIATE_STEPS.get())) {
328 scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
329 }
330 Scale s = scaleList.scaleZoomTimes(getScale(), PROP_ZOOM_RATIO.get(), times);
331 return s != null ? s.getScale() : 0;
332 }
333 }
334 return getScale() * Math.pow(PROP_ZOOM_RATIO.get(), times);
335 }
336
337 /**
338 * Get a scale snapped to native resolutions, use round method.
339 * It gives nearest step from scale list.
340 * Use round method.
341 * @param scale to snap
342 * @return snapped scale
343 */
344 public double scaleRound(double scale) {
345 return scaleSnap(scale, false);
346 }
347
348 /**
349 * Get a scale snapped to native resolutions.
350 * It gives nearest lower step from scale list, usable to fit objects.
351 * @param scale to snap
352 * @return snapped scale
353 */
354 public double scaleFloor(double scale) {
355 return scaleSnap(scale, true);
356 }
357
358 /**
359 * Get a scale snapped to native resolutions.
360 * It gives nearest lower step from scale list, usable to fit objects.
361 * @param scale to snap
362 * @param floor use floor instead of round, set true when fitting view to objects
363 * @return new scale
364 */
365 public double scaleSnap(double scale, boolean floor) {
366 if (nativeScaleLayer != null) {
367 ScaleList scaleList = nativeScaleLayer.getNativeScales();
368 if (scaleList != null) {
369 if (Boolean.TRUE.equals(PROP_ZOOM_INTERMEDIATE_STEPS.get())) {
370 scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
371 }
372 Scale snapscale = scaleList.getSnapScale(scale, PROP_ZOOM_RATIO.get(), floor);
373 return snapscale != null ? snapscale.getScale() : scale;
374 }
375 }
376 return scale;
377 }
378
379 /**
380 * Zoom in current view. Use configured zoom step and scaling settings.
381 */
382 public void zoomIn() {
383 zoomTo(state.getCenter().getEastNorth(), scaleZoomIn());
384 }
385
386 /**
387 * Zoom out current view. Use configured zoom step and scaling settings.
388 */
389 public void zoomOut() {
390 zoomTo(state.getCenter().getEastNorth(), scaleZoomOut());
391 }
392
393 protected void updateLocationState() {
394 if (isVisibleOnScreen()) {
395 state = state.usingLocation(this);
396 }
397 }
398
399 protected boolean isVisibleOnScreen() {
400 return SwingUtilities.getWindowAncestor(this) != null && isShowing();
401 }
402
403 /**
404 * Changes the projection settings used for this map view.
405 * <p>
406 * Made public temporarily, will be made private later.
407 */
408 public void fixProjection() {
409 state = state.usingProjection(ProjectionRegistry.getProjection());
410 repaint();
411 }
412
413 /**
414 * Gets the current view state. This includes the scale, the current view area and the position.
415 * @return The current state.
416 */
417 public MapViewState getState() {
418 return state;
419 }
420
421 /**
422 * Returns the text describing the given distance in the current system of measurement.
423 * @param dist The distance in metres.
424 * @return the text describing the given distance in the current system of measurement.
425 * @since 3406
426 */
427 public static String getDistText(double dist) {
428 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist);
429 }
430
431 /**
432 * Returns the text describing the given distance in the current system of measurement.
433 * @param dist The distance in metres
434 * @param format A {@link NumberFormat} to format the area value
435 * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
436 * @return the text describing the given distance in the current system of measurement.
437 * @since 7135
438 */
439 public static String getDistText(final double dist, final NumberFormat format, final double threshold) {
440 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist, format, threshold);
441 }
442
443 /**
444 * Returns the text describing the distance in meter that correspond to 100 px on screen.
445 * @return the text describing the distance in meter that correspond to 100 px on screen
446 */
447 public String getDist100PixelText() {
448 return getDistText(getDist100Pixel());
449 }
450
451 /**
452 * Get the distance in meter that correspond to 100 px on screen.
453 *
454 * @return the distance in meter that correspond to 100 px on screen
455 */
456 public double getDist100Pixel() {
457 return getDist100Pixel(true);
458 }
459
460 /**
461 * Get the distance in meter that correspond to 100 px on screen.
462 *
463 * @param alwaysPositive if true, makes sure the return value is always
464 * &gt; 0. (Two points 100 px apart can appear to be identical if the user
465 * has zoomed out a lot and the projection code does something funny.)
466 * @return the distance in meter that correspond to 100 px on screen
467 */
468 public double getDist100Pixel(boolean alwaysPositive) {
469 int w = getWidth()/2;
470 int h = getHeight()/2;
471 ILatLon ll1 = getLatLon(w-50, h);
472 ILatLon ll2 = getLatLon(w+50, h);
473 double gcd = ll1.greatCircleDistance(ll2);
474 if (alwaysPositive && gcd <= 0)
475 return 0.1;
476 return gcd;
477 }
478
479 /**
480 * Returns the current center of the viewport.
481 * <p>
482 * (Use {@link #zoomTo(EastNorth)} to the change the center.)
483 *
484 * @return the current center of the viewport
485 */
486 public EastNorth getCenter() {
487 return state.getCenter().getEastNorth();
488 }
489
490 /**
491 * Returns the current scale.
492 * <p>
493 * In east/north units per pixel.
494 *
495 * @return the current scale
496 */
497 public double getScale() {
498 return state.getScale();
499 }
500
501 /**
502 * Returns geographic coordinates from a specific pixel coordination on the screen.
503 * @param x X-Pixelposition to get coordinate from
504 * @param y Y-Pixelposition to get coordinate from
505 *
506 * @return Geographic coordinates from a specific pixel coordination on the screen.
507 */
508 public EastNorth getEastNorth(int x, int y) {
509 return state.getForView(x, y).getEastNorth();
510 }
511
512 /**
513 * Determines the projection bounds of view area.
514 * @return the projection bounds of view area
515 */
516 public ProjectionBounds getProjectionBounds() {
517 return getState().getViewArea().getProjectionBounds();
518 }
519
520 /* FIXME: replace with better method - used by MapSlider */
521 public ProjectionBounds getMaxProjectionBounds() {
522 Bounds b = getProjection().getWorldBoundsLatLon();
523 return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
524 getProjection().latlon2eastNorth(b.getMax()));
525 }
526
527 /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
528 public Bounds getRealBounds() {
529 return getState().getViewArea().getCornerBounds();
530 }
531
532 /**
533 * Returns unprojected geographic coordinates for a specific pixel position on the screen.
534 * @param x X-Pixelposition to get coordinate from
535 * @param y Y-Pixelposition to get coordinate from
536 *
537 * @return Geographic unprojected coordinates from a specific pixel position on the screen.
538 */
539 public LatLon getLatLon(int x, int y) {
540 return getProjection().eastNorth2latlon(getEastNorth(x, y));
541 }
542
543 /**
544 * Returns unprojected geographic coordinates for a specific pixel position on the screen.
545 * @param x X-Pixelposition to get coordinate from
546 * @param y Y-Pixelposition to get coordinate from
547 *
548 * @return Geographic unprojected coordinates from a specific pixel position on the screen.
549 */
550 public LatLon getLatLon(double x, double y) {
551 return getLatLon((int) x, (int) y);
552 }
553
554 /**
555 * Determines the projection bounds of given rectangle.
556 * @param r rectangle
557 * @return the projection bounds of {@code r}
558 */
559 public ProjectionBounds getProjectionBounds(Rectangle r) {
560 return getState().getViewArea(r).getProjectionBounds();
561 }
562
563 /**
564 * Returns minimum bounds that will cover a given rectangle.
565 * @param r rectangle
566 * @return Minimum bounds that will cover rectangle
567 */
568 public Bounds getLatLonBounds(Rectangle r) {
569 return ProjectionRegistry.getProjection().getLatLonBoundsBox(getProjectionBounds(r));
570 }
571
572 /**
573 * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
574 * @return The affine transform.
575 */
576 public AffineTransform getAffineTransform() {
577 return getState().getAffineTransform();
578 }
579
580 /**
581 * Return the point on the screen where this Coordinate would be.
582 * @param p The point, where this geopoint would be drawn.
583 * @return The point on screen where "point" would be drawn, relative to the own top/left.
584 */
585 public Point2D getPoint2D(EastNorth p) {
586 if (null == p)
587 return new Point();
588 return getState().getPointFor(p).getInView();
589 }
590
591 /**
592 * Return the point on the screen where this Coordinate would be.
593 * <p>
594 * Alternative: {@link #getState()}, then {@link MapViewState#getPointFor(ILatLon)}
595 * @param latlon The point, where this geopoint would be drawn.
596 * @return The point on screen where "point" would be drawn, relative to the own top/left.
597 */
598 public Point2D getPoint2D(ILatLon latlon) {
599 if (latlon == null) {
600 return new Point();
601 } else {
602 return getPoint2D(latlon.getEastNorth(ProjectionRegistry.getProjection()));
603 }
604 }
605
606 /**
607 * Return the point on the screen where this Coordinate would be.
608 * <p>
609 * Alternative: {@link #getState()}, then {@link MapViewState#getPointFor(ILatLon)}
610 * @param latlon The point, where this geopoint would be drawn.
611 * @return The point on screen where "point" would be drawn, relative to the own top/left.
612 */
613 public Point2D getPoint2D(LatLon latlon) {
614 return getPoint2D((ILatLon) latlon);
615 }
616
617 /**
618 * Return the point on the screen where this Node would be.
619 * <p>
620 * Alternative: {@link #getState()}, then {@link MapViewState#getPointFor(ILatLon)}
621 * @param n The node, where this geopoint would be drawn.
622 * @return The point on screen where "node" would be drawn, relative to the own top/left.
623 */
624 public Point2D getPoint2D(Node n) {
625 return getPoint2D(n.getEastNorth());
626 }
627
628 /**
629 * looses precision, may overflow (depends on p and current scale)
630 * @param p east/north
631 * @return point
632 * @see #getPoint2D(EastNorth)
633 */
634 public Point getPoint(EastNorth p) {
635 Point2D d = getPoint2D(p);
636 return new Point((int) d.getX(), (int) d.getY());
637 }
638
639 /**
640 * looses precision, may overflow (depends on p and current scale)
641 * @param latlon lat/lon
642 * @return point
643 * @see #getPoint2D(LatLon)
644 * @since 12725
645 */
646 public Point getPoint(ILatLon latlon) {
647 Point2D d = getPoint2D(latlon);
648 return new Point((int) d.getX(), (int) d.getY());
649 }
650
651 /**
652 * looses precision, may overflow (depends on p and current scale)
653 * @param latlon lat/lon
654 * @return point
655 * @see #getPoint2D(LatLon)
656 */
657 public Point getPoint(LatLon latlon) {
658 return getPoint((ILatLon) latlon);
659 }
660
661 /**
662 * looses precision, may overflow (depends on p and current scale)
663 * @param n node
664 * @return point
665 * @see #getPoint2D(Node)
666 */
667 public Point getPoint(Node n) {
668 Point2D d = getPoint2D(n);
669 return new Point((int) d.getX(), (int) d.getY());
670 }
671
672 /**
673 * Zoom to the given coordinate and scale.
674 *
675 * @param newCenter The center x-value (easting) to zoom to.
676 * @param newScale The scale to use.
677 */
678 public void zoomTo(EastNorth newCenter, double newScale) {
679 zoomTo(newCenter, newScale, false);
680 }
681
682 /**
683 * Zoom to the given coordinate and scale.
684 *
685 * @param center The center x-value (easting) to zoom to.
686 * @param scale The scale to use.
687 * @param initial true if this call initializes the viewport.
688 */
689 public void zoomTo(EastNorth center, double scale, boolean initial) {
690 Bounds b = getProjection().getWorldBoundsLatLon();
691 ProjectionBounds pb = getProjection().getWorldBoundsBoxEastNorth();
692 double newScale = scale;
693 int width = getWidth();
694 int height = getHeight();
695
696 // make sure, the center of the screen is within projection bounds
697 double east = center.east();
698 double north = center.north();
699 east = Math.max(east, pb.minEast);
700 east = Math.min(east, pb.maxEast);
701 north = Math.max(north, pb.minNorth);
702 north = Math.min(north, pb.maxNorth);
703 EastNorth newCenter = new EastNorth(east, north);
704
705 // don't zoom out too much, the world bounds should be at least
706 // half the size of the screen
707 double pbHeight = pb.maxNorth - pb.minNorth;
708 if (height > 0 && 2 * pbHeight < height * newScale) {
709 double newScaleH = 2 * pbHeight / height;
710 double pbWidth = pb.maxEast - pb.minEast;
711 if (width > 0 && 2 * pbWidth < width * newScale) {
712 double newScaleW = 2 * pbWidth / width;
713 newScale = Math.max(newScaleH, newScaleW);
714 }
715 }
716
717 // don't zoom in too much, minimum: 100 px = 1 cm
718 LatLon ll1 = getLatLon(width / 2 - 50, height / 2);
719 LatLon ll2 = getLatLon(width / 2 + 50, height / 2);
720 if (ll1.isValid() && ll2.isValid() && b.contains(ll1) && b.contains(ll2)) {
721 double dm = ll1.greatCircleDistance((ILatLon) ll2);
722 double den = 100 * getScale();
723 double scaleMin = 0.01 * den / dm / 100;
724 if (newScale < scaleMin && !Double.isInfinite(scaleMin)) {
725 newScale = scaleMin;
726 }
727 }
728
729 // snap scale to imagery if needed
730 newScale = scaleRound(newScale);
731
732 // Align to the pixel grid:
733 // This is a sub-pixel correction to ensure consistent drawing at a certain scale.
734 // For example take 2 nodes, that have a distance of exactly 2.6 pixels.
735 // Depending on the offset, the distance in rounded or truncated integer
736 // pixels will be 2 or 3. It is preferable to have a consistent distance
737 // and not switch back and forth as the viewport moves. This can be achieved by
738 // locking an arbitrary point to integer pixel coordinates. (Here the EastNorth
739 // origin is used as reference point.)
740 // Note that the normal right mouse button drag moves the map by integer pixel
741 // values, so it is not an issue in this case. It only shows when zooming
742 // in & back out, etc.
743 MapViewState mvs = getState().usingScale(newScale);
744 mvs = mvs.movedTo(mvs.getCenter(), newCenter);
745 Point2D enOrigin = mvs.getPointFor(new EastNorth(0, 0)).getInView();
746 // as a result of the alignment, it is common to round "half integer" values
747 // like 1.49999, which is numerically unstable; add small epsilon to resolve this
748 Point2D enOriginAligned = new Point2D.Double(
749 Math.round(enOrigin.getX()) + ALIGNMENT_EPSILON,
750 Math.round(enOrigin.getY()) + ALIGNMENT_EPSILON);
751 EastNorth enShift = mvs.getForView(enOriginAligned.getX(), enOriginAligned.getY()).getEastNorth();
752 newCenter = newCenter.subtract(enShift);
753
754 EastNorth oldCenter = getCenter();
755 if (!newCenter.equals(oldCenter) || !Utils.equalsEpsilon(getScale(), newScale)) {
756 if (!initial) {
757 pushZoomUndo(oldCenter, getScale());
758 }
759 zoomNoUndoTo(newCenter, newScale, initial);
760 }
761 }
762
763 /**
764 * Zoom to the given coordinate without adding to the zoom undo buffer.
765 *
766 * @param newCenter The center x-value (easting) to zoom to.
767 * @param newScale The scale to use.
768 * @param initial true if this call initializes the viewport.
769 */
770 private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) {
771 if (!Utils.equalsEpsilon(getScale(), newScale)) {
772 state = state.usingScale(newScale);
773 }
774 if (!newCenter.equals(getCenter())) {
775 state = state.movedTo(state.getCenter(), newCenter);
776 }
777 if (!initial) {
778 repaint();
779 fireZoomChanged();
780 }
781 }
782
783 /**
784 * Zoom to given east/north.
785 * @param newCenter new center coordinates
786 */
787 public void zoomTo(EastNorth newCenter) {
788 zoomTo(newCenter, getScale());
789 }
790
791 /**
792 * Zoom to given lat/lon.
793 * @param newCenter new center coordinates
794 * @since 12725
795 */
796 public void zoomTo(ILatLon newCenter) {
797 zoomTo(getProjection().latlon2eastNorth(newCenter));
798 }
799
800 /**
801 * Zoom to given lat/lon.
802 * @param newCenter new center coordinates
803 */
804 public void zoomTo(LatLon newCenter) {
805 zoomTo((ILatLon) newCenter);
806 }
807
808 /**
809 * Thread class for smooth scrolling. Made a separate class, so we can safely terminate it.
810 */
811 private class SmoothScrollThread extends Thread {
812 private boolean doStop;
813 private final EastNorth oldCenter = getCenter();
814 private final EastNorth finalNewCenter;
815 private final long frames;
816 private final long sleepTime;
817
818 SmoothScrollThread(EastNorth newCenter, long frameNum, int fps) {
819 super("smooth-scroller");
820 finalNewCenter = newCenter;
821 frames = frameNum;
822 sleepTime = 1000L / fps;
823 }
824
825 @Override
826 public void run() {
827 try {
828 for (int i = 0; i < frames && !doStop; i++) {
829 final EastNorth z = oldCenter.interpolate(finalNewCenter, (1.0+i) / frames);
830 GuiHelper.runInEDTAndWait(() -> zoomTo(z));
831 Thread.sleep(sleepTime);
832 }
833 } catch (InterruptedException ex) {
834 Logging.warn("Interruption during smooth scrolling");
835 }
836 }
837
838 public void stopIt() {
839 doStop = true;
840 }
841 }
842
843 /**
844 * Create a thread that moves the viewport to the given center in an animated fashion.
845 * @param newCenter new east/north center
846 */
847 public void smoothScrollTo(EastNorth newCenter) {
848 final EastNorth oldCenter = getCenter();
849 if (!newCenter.equals(oldCenter)) {
850 final int fps = Config.getPref().getInt("smooth.scroll.fps", 20); // animation frames per second
851 final int speed = Config.getPref().getInt("smooth.scroll.speed", 1500); // milliseconds for full-screen-width pan
852 final int maxtime = Config.getPref().getInt("smooth.scroll.maxtime", 5000); // milliseconds maximum scroll time
853 final double distance = newCenter.distance(oldCenter) / getScale();
854 double milliseconds = distance / getWidth() * speed;
855 if (milliseconds > maxtime) { // prevent overlong scroll time, speed up if necessary
856 milliseconds = maxtime;
857 }
858
859 ThreadGroup group = Thread.currentThread().getThreadGroup();
860 Thread[] threads = new Thread[group.activeCount()];
861 group.enumerate(threads, true);
862 boolean stopped = false;
863 for (Thread t : threads) {
864 if (t instanceof SmoothScrollThread) {
865 ((SmoothScrollThread) t).stopIt();
866 /* handle this case outside in case there is more than one smooth thread */
867 stopped = true;
868 }
869 }
870 if (stopped && milliseconds > maxtime/2.0) { /* we aren't fast enough, skip smooth */
871 Logging.warn("Skip smooth scrolling");
872 zoomTo(newCenter);
873 } else {
874 long frames = Math.round(milliseconds * fps / 1000);
875 if (frames <= 1)
876 zoomTo(newCenter);
877 else
878 new SmoothScrollThread(newCenter, frames, fps).start();
879 }
880 }
881 }
882
883 public void zoomManyTimes(double x, double y, int times) {
884 double oldScale = getScale();
885 double newScale = scaleZoomManyTimes(times);
886 zoomToFactor(x, y, newScale / oldScale);
887 }
888
889 public void zoomToFactor(double x, double y, double factor) {
890 double newScale = getScale()*factor;
891 EastNorth oldUnderMouse = getState().getForView(x, y).getEastNorth();
892 MapViewState newState = getState().usingScale(newScale);
893 newState = newState.movedTo(newState.getForView(x, y), oldUnderMouse);
894 zoomTo(newState.getCenter().getEastNorth(), newScale);
895 }
896
897 public void zoomToFactor(EastNorth newCenter, double factor) {
898 zoomTo(newCenter, getScale()*factor);
899 }
900
901 public void zoomToFactor(double factor) {
902 zoomTo(getCenter(), getScale()*factor);
903 }
904
905 /**
906 * Zoom to given projection bounds.
907 * @param box new projection bounds
908 */
909 public void zoomTo(ProjectionBounds box) {
910 double newScale = box.getScale(getWidth(), getHeight());
911 newScale = scaleFloor(newScale);
912 zoomTo(box.getCenter(), newScale);
913 }
914
915 /**
916 * Zoom to given bounds.
917 * @param box new bounds
918 */
919 public void zoomTo(Bounds box) {
920 zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
921 getProjection().latlon2eastNorth(box.getMax())));
922 }
923
924 /**
925 * Zoom to given viewport data.
926 * @param viewport new viewport data
927 */
928 public void zoomTo(ViewportData viewport) {
929 if (viewport == null) return;
930 if (viewport.getBounds() != null) {
931 if (!viewport.getBounds().hasExtend()) {
932 // see #18623
933 BoundingXYVisitor v = new BoundingXYVisitor();
934 v.visit(viewport.getBounds());
935 zoomTo(v);
936 } else {
937 zoomTo(viewport.getBounds());
938 }
939
940 } else {
941 zoomTo(viewport.getCenter(), viewport.getScale(), true);
942 }
943 }
944
945 /**
946 * Set the new dimension to the view.
947 * @param v box to zoom to
948 */
949 public void zoomTo(BoundingXYVisitor v) {
950 if (v == null) {
951 v = new BoundingXYVisitor();
952 }
953 if (v.getBounds() == null) {
954 v.visit(getProjection().getWorldBoundsLatLon());
955 }
956
957 // increase bbox. This is required
958 // especially if the bbox contains one single node, but helpful
959 // in most other cases as well.
960 // Do not zoom if the current scale covers the selection, #16706
961 final MapView mapView = MainApplication.getMap().mapView;
962 final double mapScale = mapView.getScale();
963 final double minScale = v.getBounds().getScale(mapView.getWidth(), mapView.getHeight());
964 v.enlargeBoundingBoxLogarithmically();
965 final double maxScale = v.getBounds().getScale(mapView.getWidth(), mapView.getHeight());
966 if (minScale <= mapScale && mapScale < maxScale) {
967 mapView.zoomTo(v.getBounds().getCenter());
968 } else {
969 zoomTo(v.getBounds());
970 }
971 }
972
973 private static class ZoomData {
974 private final EastNorth center;
975 private final double scale;
976
977 ZoomData(EastNorth center, double scale) {
978 this.center = center;
979 this.scale = scale;
980 }
981
982 public EastNorth getCenterEastNorth() {
983 return center;
984 }
985
986 public double getScale() {
987 return scale;
988 }
989 }
990
991 private final transient Stack<ZoomData> zoomUndoBuffer = new Stack<>();
992 private final transient Stack<ZoomData> zoomRedoBuffer = new Stack<>();
993 private long zoomTimestamp = System.currentTimeMillis();
994
995 private void pushZoomUndo(EastNorth center, double scale) {
996 long now = System.currentTimeMillis();
997 if ((now - zoomTimestamp) > (Config.getPref().getDouble("zoom.undo.delay", 1.0) * 1000)) {
998 zoomUndoBuffer.push(new ZoomData(center, scale));
999 if (zoomUndoBuffer.size() > Config.getPref().getInt("zoom.undo.max", 50)) {
1000 zoomUndoBuffer.remove(0);
1001 }
1002 zoomRedoBuffer.clear();
1003 }
1004 zoomTimestamp = now;
1005 }
1006
1007 /**
1008 * Zoom to previous location.
1009 */
1010 public void zoomPrevious() {
1011 if (!zoomUndoBuffer.isEmpty()) {
1012 ZoomData zoom = zoomUndoBuffer.pop();
1013 zoomRedoBuffer.push(new ZoomData(getCenter(), getScale()));
1014 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
1015 }
1016 }
1017
1018 /**
1019 * Zoom to next location.
1020 */
1021 public void zoomNext() {
1022 if (!zoomRedoBuffer.isEmpty()) {
1023 ZoomData zoom = zoomRedoBuffer.pop();
1024 zoomUndoBuffer.push(new ZoomData(getCenter(), getScale()));
1025 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
1026 }
1027 }
1028
1029 /**
1030 * Determines if zoom history contains "undo" entries.
1031 * @return {@code true} if zoom history contains "undo" entries
1032 */
1033 public boolean hasZoomUndoEntries() {
1034 return !zoomUndoBuffer.isEmpty();
1035 }
1036
1037 /**
1038 * Determines if zoom history contains "redo" entries.
1039 * @return {@code true} if zoom history contains "redo" entries
1040 */
1041 public boolean hasZoomRedoEntries() {
1042 return !zoomRedoBuffer.isEmpty();
1043 }
1044
1045 private BBox getBBox(Point p, int snapDistance) {
1046 return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
1047 getLatLon(p.x + snapDistance, p.y + snapDistance));
1048 }
1049
1050 /**
1051 * The *result* does not depend on the current map selection state, neither does the result *order*.
1052 * It solely depends on the distance to point p.
1053 * @param p point
1054 * @param predicate predicate to match
1055 *
1056 * @return a sorted map with the keys representing the distance of their associated nodes to point p.
1057 */
1058 private Map<Double, List<Node>> getNearestNodesImpl(Point p, Predicate<OsmPrimitive> predicate) {
1059 Map<Double, List<Node>> nearestMap = new TreeMap<>();
1060 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1061
1062 if (ds != null) {
1063 double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
1064 snapDistanceSq *= snapDistanceSq;
1065
1066 for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
1067 if (predicate.test(n)
1068 && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
1069 nearestMap.computeIfAbsent(dist, k -> new LinkedList<>()).add(n);
1070 }
1071 }
1072 }
1073
1074 return nearestMap;
1075 }
1076
1077 /**
1078 * The *result* does not depend on the current map selection state,
1079 * neither does the result *order*.
1080 * It solely depends on the distance to point p.
1081 *
1082 * @param p the point for which to search the nearest segment.
1083 * @param ignore a collection of nodes which are not to be returned.
1084 * @param predicate the returned objects have to fulfill certain properties.
1085 *
1086 * @return All nodes nearest to point p that are in a belt from
1087 * dist(nearest) to dist(nearest)+4px around p and
1088 * that are not in ignore.
1089 */
1090 public final List<Node> getNearestNodes(Point p,
1091 Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
1092 List<Node> nearestList = Collections.emptyList();
1093
1094 if (ignore == null) {
1095 ignore = Collections.emptySet();
1096 }
1097
1098 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1099 if (!nlists.isEmpty()) {
1100 Double minDistSq = null;
1101 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1102 Double distSq = entry.getKey();
1103 List<Node> nlist = entry.getValue();
1104
1105 // filter nodes to be ignored before determining minDistSq..
1106 nlist.removeAll(ignore);
1107 if (minDistSq == null) {
1108 if (!nlist.isEmpty()) {
1109 minDistSq = distSq;
1110 nearestList = new ArrayList<>(nlist);
1111 }
1112 } else {
1113 if (distSq-minDistSq < 16) {
1114 nearestList.addAll(nlist);
1115 }
1116 }
1117 }
1118 }
1119
1120 return nearestList;
1121 }
1122
1123 /**
1124 * The *result* does not depend on the current map selection state,
1125 * neither does the result *order*.
1126 * It solely depends on the distance to point p.
1127 *
1128 * @param p the point for which to search the nearest segment.
1129 * @param predicate the returned objects have to fulfill certain properties.
1130 *
1131 * @return All nodes nearest to point p that are in a belt from
1132 * dist(nearest) to dist(nearest)+4px around p.
1133 * @see #getNearestNodes(Point, Collection, Predicate)
1134 */
1135 public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
1136 return getNearestNodes(p, null, predicate);
1137 }
1138
1139 /**
1140 * The *result* depends on the current map selection state IF use_selected is true.
1141 * <p>
1142 * If more than one node within node.snap-distance pixels is found,
1143 * the nearest node selected is returned IF use_selected is true.
1144 * <p>
1145 * Else the nearest new/id=0 node within about the same distance
1146 * as the true nearest node is returned.
1147 * <p>
1148 * If no such node is found either, the true nearest node to p is returned.
1149 * <p>
1150 * Finally, if a node is not found at all, {@code null} is returned.
1151 *
1152 * @param p the screen point
1153 * @param predicate this parameter imposes a condition on the returned object, e.g.
1154 * give the nearest node that is tagged.
1155 * @param useSelected make search depend on selection
1156 *
1157 * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1158 */
1159 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1160 return getNearestNode(p, predicate, useSelected, null);
1161 }
1162
1163 /**
1164 * The *result* depends on the current map selection state IF use_selected is true
1165 * <p>
1166 * If more than one node within node.snap-distance pixels is found,
1167 * the nearest node selected is returned IF use_selected is true.
1168 * <p>
1169 * If there are no selected nodes near that point, the node that is related to some of the preferredRefs
1170 * <p>
1171 * Else the nearest new/id=0 node within about the same distance
1172 * as the true nearest node is returned.
1173 * <p>
1174 * If no such node is found either, the true nearest node to p is returned.
1175 * <p>
1176 * Finally, if a node is not found at all, {@code null} is returned.
1177 *
1178 * @param p the screen point
1179 * @param predicate this parameter imposes a condition on the returned object, e.g.
1180 * give the nearest node that is tagged.
1181 * @param useSelected make search depend on selection
1182 * @param preferredRefs primitives, whose nodes we prefer
1183 *
1184 * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1185 * @since 6065
1186 */
1187 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
1188 boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1189
1190 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1191 if (nlists.isEmpty()) return null;
1192
1193 if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
1194 Node ntsel = null, ntnew = null, ntref = null;
1195 boolean useNtsel = useSelected;
1196 double minDistSq = nlists.keySet().iterator().next();
1197
1198 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1199 Double distSq = entry.getKey();
1200 for (Node nd : entry.getValue()) {
1201 // find the nearest selected node
1202 if (ntsel == null && nd.isSelected()) {
1203 ntsel = nd;
1204 // if there are multiple nearest nodes, prefer the one
1205 // that is selected. This is required in order to drag
1206 // the selected node if multiple nodes have the same
1207 // coordinates (e.g. after unglue)
1208 useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
1209 }
1210 if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
1211 List<OsmPrimitive> ndRefs = nd.getReferrers();
1212 if (preferredRefs.stream().anyMatch(ndRefs::contains)) {
1213 ntref = nd;
1214 }
1215 }
1216 // find the nearest newest node that is within about the same
1217 // distance as the true nearest node
1218 if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
1219 ntnew = nd;
1220 }
1221 }
1222 }
1223
1224 // take nearest selected, nearest new or true nearest node to p, in that order
1225 if (ntsel != null && useNtsel)
1226 return ntsel;
1227 if (ntref != null)
1228 return ntref;
1229 if (ntnew != null)
1230 return ntnew;
1231 return nlists.values().iterator().next().get(0);
1232 }
1233
1234 /**
1235 * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
1236 * @param p the screen point
1237 * @param predicate this parameter imposes a condition on the returned object, e.g.
1238 * give the nearest node that is tagged.
1239 *
1240 * @return The nearest node to point p.
1241 */
1242 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
1243 return getNearestNode(p, predicate, true);
1244 }
1245
1246 /**
1247 * The *result* does not depend on the current map selection state, neither does the result *order*.
1248 * It solely depends on the distance to point p.
1249 * @param p the screen point
1250 * @param predicate this parameter imposes a condition on the returned object, e.g.
1251 * give the nearest node that is tagged.
1252 *
1253 * @return a sorted map with the keys representing the perpendicular
1254 * distance of their associated way segments to point p.
1255 */
1256 private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
1257 Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
1258 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1259
1260 if (ds != null) {
1261 double snapDistanceSq = Config.getPref().getInt("mappaint.segment.snap-distance", 10);
1262 snapDistanceSq *= snapDistanceSq;
1263
1264 for (Way w : ds.searchWays(getBBox(p, Config.getPref().getInt("mappaint.segment.snap-distance", 10)))) {
1265 if (!predicate.test(w)) {
1266 continue;
1267 }
1268 Node lastN = null;
1269 int i = -2;
1270 for (Node n : w.getNodes()) {
1271 i++;
1272 if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
1273 continue;
1274 }
1275 if (lastN == null) {
1276 lastN = n;
1277 continue;
1278 }
1279
1280 Point2D pA = getPoint2D(lastN);
1281 Point2D pB = getPoint2D(n);
1282 double c = pA.distanceSq(pB);
1283 double a = p.distanceSq(pB);
1284 double b = p.distanceSq(pA);
1285
1286 /* perpendicular distance squared
1287 * loose some precision to account for possible deviations in the calculation above
1288 * e.g. if identical (A and B) come about reversed in another way, values may differ
1289 * -- zero out least significant 32 dual digits of mantissa.
1290 */
1291 double perDistSq = Double.longBitsToDouble(
1292 Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c)
1293 >> 32 << 32); // resolution in numbers with large exponent not needed here.
1294
1295 if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
1296 nearestMap.computeIfAbsent(perDistSq, k -> new LinkedList<>()).add(new WaySegment(w, i));
1297 }
1298
1299 lastN = n;
1300 }
1301 }
1302 }
1303
1304 return nearestMap;
1305 }
1306
1307 /**
1308 * The result *order* depends on the current map selection state.
1309 * Segments within 10px of p are searched and sorted by their distance to {@code p},
1310 * then, within groups of equally distant segments, prefer those that are selected.
1311 *
1312 * @param p the point for which to search the nearest segments.
1313 * @param ignore a collection of segments which are not to be returned.
1314 * @param predicate the returned objects have to fulfill certain properties.
1315 *
1316 * @return all segments within 10px of p that are not in ignore,
1317 * sorted by their perpendicular distance.
1318 */
1319 public final List<WaySegment> getNearestWaySegments(Point p,
1320 Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
1321 List<WaySegment> nearestList = new ArrayList<>();
1322 List<WaySegment> unselected = new LinkedList<>();
1323
1324 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1325 // put selected waysegs within each distance group first
1326 // makes the order of nearestList dependent on current selection state
1327 for (WaySegment ws : wss) {
1328 (ws.getWay().isSelected() ? nearestList : unselected).add(ws);
1329 }
1330 nearestList.addAll(unselected);
1331 unselected.clear();
1332 }
1333 if (ignore != null) {
1334 nearestList.removeAll(ignore);
1335 }
1336
1337 return nearestList;
1338 }
1339
1340 /**
1341 * The result *order* depends on the current map selection state.
1342 *
1343 * @param p the point for which to search the nearest segments.
1344 * @param predicate the returned objects have to fulfill certain properties.
1345 *
1346 * @return all segments within 10px of p, sorted by their perpendicular distance.
1347 * @see #getNearestWaySegments(Point, Collection, Predicate)
1348 */
1349 public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
1350 return getNearestWaySegments(p, null, predicate);
1351 }
1352
1353 /**
1354 * The *result* depends on the current map selection state IF use_selected is true.
1355 *
1356 * @param p the point for which to search the nearest segment.
1357 * @param predicate the returned object has to fulfill certain properties.
1358 * @param useSelected whether selected way segments should be preferred.
1359 *
1360 * @return The nearest way segment to point p,
1361 * and, depending on use_selected, prefers a selected way segment, if found.
1362 * @see #getNearestWaySegments(Point, Collection, Predicate)
1363 */
1364 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1365 WaySegment wayseg = null;
1366 WaySegment ntsel = null;
1367
1368 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1369 if (wayseg != null && ntsel != null) {
1370 break;
1371 }
1372 for (WaySegment ws : wslist) {
1373 if (wayseg == null) {
1374 wayseg = ws;
1375 }
1376 if (ntsel == null && ws.getWay().isSelected()) {
1377 ntsel = ws;
1378 }
1379 }
1380 }
1381
1382 return (ntsel != null && useSelected) ? ntsel : wayseg;
1383 }
1384
1385 /**
1386 * The *result* depends on the current map selection state IF use_selected is true.
1387 *
1388 * @param p the point for which to search the nearest segment.
1389 * @param predicate the returned object has to fulfill certain properties.
1390 * @param useSelected whether selected way segments should be preferred.
1391 * @param preferredRefs - prefer segments related to these primitives, may be null
1392 *
1393 * @return The nearest way segment to point p,
1394 * and, depending on use_selected, prefers a selected way segment, if found.
1395 * Also prefers segments of ways that are related to one of preferredRefs primitives
1396 *
1397 * @see #getNearestWaySegments(Point, Collection, Predicate)
1398 * @since 6065
1399 */
1400 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
1401 boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1402 WaySegment wayseg = null;
1403 if (preferredRefs != null && preferredRefs.isEmpty())
1404 preferredRefs = null;
1405
1406 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1407 for (WaySegment ws : wslist) {
1408 if (wayseg == null) {
1409 wayseg = ws;
1410 }
1411 if (useSelected && ws.getWay().isSelected()) {
1412 return ws;
1413 }
1414 if (!Utils.isEmpty(preferredRefs)) {
1415 // prefer ways containing given nodes
1416 if (preferredRefs.contains(ws.getFirstNode()) || preferredRefs.contains(ws.getSecondNode())) {
1417 return ws;
1418 }
1419 Collection<OsmPrimitive> wayRefs = ws.getWay().getReferrers();
1420 // prefer member of the given relations
1421 for (OsmPrimitive ref: preferredRefs) {
1422 if (ref instanceof Relation && wayRefs.contains(ref)) {
1423 return ws;
1424 }
1425 }
1426 }
1427 }
1428 }
1429 return wayseg;
1430 }
1431
1432 /**
1433 * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
1434 * @param p the point for which to search the nearest segment.
1435 * @param predicate the returned object has to fulfill certain properties.
1436 *
1437 * @return The nearest way segment to point p.
1438 */
1439 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
1440 return getNearestWaySegment(p, predicate, true);
1441 }
1442
1443 /**
1444 * The *result* does not depend on the current map selection state,
1445 * neither does the result *order*.
1446 * It solely depends on the perpendicular distance to point p.
1447 *
1448 * @param p the point for which to search the nearest ways.
1449 * @param ignore a collection of ways which are not to be returned.
1450 * @param predicate the returned object has to fulfill certain properties.
1451 *
1452 * @return all nearest ways to the screen point given that are not in ignore.
1453 * @see #getNearestWaySegments(Point, Collection, Predicate)
1454 */
1455 public final List<Way> getNearestWays(Point p,
1456 Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
1457 Set<Way> wset = new HashSet<>();
1458
1459 List<Way> nearestList = getNearestWaySegmentsImpl(p, predicate).values().stream()
1460 .flatMap(Collection::stream)
1461 .filter(ws -> wset.add(ws.getWay()))
1462 .map(IWaySegment::getWay)
1463 .collect(Collectors.toList());
1464 if (ignore != null) {
1465 nearestList.removeAll(ignore);
1466 }
1467
1468 return nearestList;
1469 }
1470
1471 /**
1472 * The *result* does not depend on the current map selection state,
1473 * neither does the result *order*.
1474 * It solely depends on the perpendicular distance to point p.
1475 *
1476 * @param p the point for which to search the nearest ways.
1477 * @param predicate the returned object has to fulfill certain properties.
1478 *
1479 * @return all nearest ways to the screen point given.
1480 * @see #getNearestWays(Point, Collection, Predicate)
1481 */
1482 public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1483 return getNearestWays(p, null, predicate);
1484 }
1485
1486 /**
1487 * The *result* depends on the current map selection state.
1488 *
1489 * @param p the point for which to search the nearest segment.
1490 * @param predicate the returned object has to fulfill certain properties.
1491 *
1492 * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
1493 * @see #getNearestWaySegment(Point, Predicate)
1494 */
1495 public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
1496 WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
1497 return (nearestWaySeg == null) ? null : nearestWaySeg.getWay();
1498 }
1499
1500 /**
1501 * The *result* does not depend on the current map selection state,
1502 * neither does the result *order*.
1503 * It solely depends on the distance to point p.
1504 * <p>
1505 * First, nodes will be searched. If there are nodes within BBox found,
1506 * return a collection of those nodes only.
1507 * <p>
1508 * If no nodes are found, search for nearest ways. If there are ways
1509 * within BBox found, return a collection of those ways only.
1510 * <p>
1511 * If nothing is found, return an empty collection.
1512 *
1513 * @param p The point on screen.
1514 * @param ignore a collection of ways which are not to be returned.
1515 * @param predicate the returned object has to fulfill certain properties.
1516 *
1517 * @return Primitives nearest to the given screen point that are not in ignore.
1518 * @see #getNearestNodes(Point, Collection, Predicate)
1519 * @see #getNearestWays(Point, Collection, Predicate)
1520 */
1521 public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
1522 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1523 List<OsmPrimitive> nearestList = Collections.emptyList();
1524 OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
1525
1526 if (osm != null) {
1527 if (osm instanceof Node) {
1528 nearestList = new ArrayList<>(getNearestNodes(p, predicate));
1529 } else if (osm instanceof Way) {
1530 nearestList = new ArrayList<>(getNearestWays(p, predicate));
1531 }
1532 if (ignore != null) {
1533 nearestList.removeAll(ignore);
1534 }
1535 }
1536
1537 return nearestList;
1538 }
1539
1540 /**
1541 * The *result* does not depend on the current map selection state,
1542 * neither does the result *order*.
1543 * It solely depends on the distance to point p.
1544 *
1545 * @param p The point on screen.
1546 * @param predicate the returned object has to fulfill certain properties.
1547 * @return Primitives nearest to the given screen point.
1548 * @see #getNearestNodesOrWays(Point, Collection, Predicate)
1549 */
1550 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1551 return getNearestNodesOrWays(p, null, predicate);
1552 }
1553
1554 /**
1555 * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
1556 * It decides, whether to yield the node to be tested or look for further (way) candidates.
1557 *
1558 * @param osm node to check
1559 * @param p point clicked
1560 * @param useSelected whether to prefer selected nodes
1561 * @return true, if the node fulfills the properties of the function body
1562 */
1563 private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
1564 if (osm != null) {
1565 if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
1566 if (osm.isTagged()) return true;
1567 if (useSelected && osm.isSelected()) return true;
1568 }
1569 return false;
1570 }
1571
1572 /**
1573 * The *result* depends on the current map selection state IF use_selected is true.
1574 * <p>
1575 * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
1576 * the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)}
1577 * to find the nearest selected way.
1578 * <p>
1579 * IF use_selected is false, or if no selected primitive was found, do the following.
1580 * <p>
1581 * If the nearest node found is within 4px of p, simply take it.
1582 * Else, find the nearest way segment. Then, if p is closer to its
1583 * middle than to the node, take the way segment, else take the node.
1584 * <p>
1585 * Finally, if no nearest primitive is found at all, return {@code null}.
1586 *
1587 * @param p The point on screen.
1588 * @param predicate the returned object has to fulfill certain properties.
1589 * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
1590 *
1591 * @return A primitive within snap-distance to point p,
1592 * that is chosen by the algorithm described.
1593 * @see #getNearestNode(Point, Predicate)
1594 * @see #getNearestWay(Point, Predicate)
1595 */
1596 public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1597 Collection<OsmPrimitive> sel;
1598 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1599 if (useSelected && ds != null) {
1600 sel = ds.getSelected();
1601 } else {
1602 sel = null;
1603 }
1604 OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
1605
1606 if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
1607 WaySegment ws;
1608 if (useSelected) {
1609 ws = getNearestWaySegment(p, predicate, useSelected, sel);
1610 } else {
1611 ws = getNearestWaySegment(p, predicate, useSelected);
1612 }
1613 if (ws == null) return osm;
1614
1615 if ((ws.getWay().isSelected() && useSelected) || osm == null) {
1616 // either (no _selected_ nearest node found, if desired) or no nearest node was found
1617 osm = ws.getWay();
1618 } else {
1619 int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1620 maxWaySegLenSq *= maxWaySegLenSq;
1621
1622 Point2D wp1 = getPoint2D(ws.getFirstNode());
1623 Point2D wp2 = getPoint2D(ws.getSecondNode());
1624
1625 // is wayseg shorter than maxWaySegLenSq and
1626 // is p closer to the middle of wayseg than to the nearest node?
1627 if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1628 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
1629 osm = ws.getWay();
1630 }
1631 }
1632 return osm;
1633 }
1634
1635 /**
1636 * if r = 0 returns a, if r=1 returns b,
1637 * if r = 0.5 returns center between a and b, etc.
1638 *
1639 * @param r scale value
1640 * @param a root of vector
1641 * @param b vector
1642 * @return new point at a + r*(ab)
1643 */
1644 public static Point2D project(double r, Point2D a, Point2D b) {
1645 Point2D ret = null;
1646
1647 if (a != null && b != null) {
1648 ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1649 a.getY() + r*(b.getY()-a.getY()));
1650 }
1651 return ret;
1652 }
1653
1654 /**
1655 * The *result* does not depend on the current map selection state, neither does the result *order*.
1656 * It solely depends on the distance to point p.
1657 *
1658 * @param p The point on screen.
1659 * @param ignore a collection of ways which are not to be returned.
1660 * @param predicate the returned object has to fulfill certain properties.
1661 *
1662 * @return a list of all objects that are nearest to point p and
1663 * not in ignore or an empty list if nothing was found.
1664 */
1665 public final List<OsmPrimitive> getAllNearest(Point p,
1666 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1667 Set<Way> wset = new HashSet<>();
1668
1669 // add nearby ways
1670 List<OsmPrimitive> nearestList = getNearestWaySegmentsImpl(p, predicate).values().stream()
1671 .flatMap(Collection::stream)
1672 .filter(ws -> wset.add(ws.getWay()))
1673 .map(IWaySegment::getWay)
1674 .collect(Collectors.toList());
1675
1676 // add nearby nodes
1677 getNearestNodesImpl(p, predicate).values()
1678 .forEach(nearestList::addAll);
1679
1680 // add parent relations of nearby nodes and ways
1681 Set<OsmPrimitive> parentRelations = nearestList.stream()
1682 .flatMap(o -> o.referrers(Relation.class))
1683 .filter(predicate)
1684 .collect(Collectors.toSet());
1685 nearestList.addAll(parentRelations);
1686
1687 if (ignore != null) {
1688 nearestList.removeAll(ignore);
1689 }
1690
1691 return nearestList;
1692 }
1693
1694 /**
1695 * The *result* does not depend on the current map selection state, neither does the result *order*.
1696 * It solely depends on the distance to point p.
1697 *
1698 * @param p The point on screen.
1699 * @param predicate the returned object has to fulfill certain properties.
1700 *
1701 * @return a list of all objects that are nearest to point p
1702 * or an empty list if nothing was found.
1703 * @see #getAllNearest(Point, Collection, Predicate)
1704 */
1705 public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1706 return getAllNearest(p, null, predicate);
1707 }
1708
1709 /**
1710 * Returns the projection to be used in calculating stuff.
1711 * @return The projection to be used in calculating stuff.
1712 */
1713 public Projection getProjection() {
1714 return state.getProjection();
1715 }
1716
1717 @Override
1718 public String helpTopic() {
1719 String n = getClass().getName();
1720 return n.substring(n.lastIndexOf('.')+1);
1721 }
1722
1723 /**
1724 * Return an ID which is unique as long as viewport dimensions are the same
1725 * @return A unique ID, as long as viewport dimensions are the same
1726 */
1727 public int getViewID() {
1728 EastNorth center = getCenter();
1729 String x = String.valueOf(center.east()) +
1730 '_' + center.north() +
1731 '_' + getScale() +
1732 '_' + getWidth() +
1733 '_' + getHeight() +
1734 '_' + getProjection();
1735 CRC32 id = new CRC32();
1736 id.update(x.getBytes(StandardCharsets.UTF_8));
1737 return (int) id.getValue();
1738 }
1739
1740 /**
1741 * Set new cursor.
1742 * @param cursor The new cursor to use.
1743 * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1744 */
1745 public void setNewCursor(Cursor cursor, Object reference) {
1746 cursorManager.setNewCursor(cursor, reference);
1747 }
1748
1749 /**
1750 * Set new cursor.
1751 * @param cursor the type of predefined cursor
1752 * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1753 */
1754 public void setNewCursor(int cursor, Object reference) {
1755 setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1756 }
1757
1758 /**
1759 * Remove the new cursor and reset to previous
1760 * @param reference Cursor reference
1761 */
1762 public void resetCursor(Object reference) {
1763 cursorManager.resetCursor(reference);
1764 }
1765
1766 /**
1767 * Gets the cursor manager that is used for this NavigatableComponent.
1768 * @return The cursor manager.
1769 */
1770 public CursorManager getCursorManager() {
1771 return cursorManager;
1772 }
1773
1774 /**
1775 * Get a max scale for projection that describes world in 1/512 of the projection unit
1776 * @return max scale
1777 */
1778 public double getMaxScale() {
1779 ProjectionBounds world = getMaxProjectionBounds();
1780 return Math.max(
1781 world.maxNorth-world.minNorth,
1782 world.maxEast-world.minEast
1783 )/512;
1784 }
1785
1786 /**
1787 * Listener for mouse movement events. Used to detect when primitives are being hovered over with the mouse pointer
1788 * so that registered {@link PrimitiveHoverListener}s can be notified.
1789 */
1790 private class PrimitiveHoverMouseListener extends MouseAdapter {
1791 @Override
1792 public void mouseMoved(MouseEvent e) {
1793 OsmPrimitive hovered = getNearestNodeOrWay(e.getPoint(), isSelectablePredicate, true);
1794 updateHoveredPrimitive(hovered, e);
1795 }
1796
1797 @Override
1798 public void mouseExited(MouseEvent e) {
1799 updateHoveredPrimitive(null, e);
1800 }
1801 }
1802}
Note: See TracBrowser for help on using the repository browser.