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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 15 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 67.4 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;
1064 double snapDistanceSq = PROP_SNAP_DISTANCE.get();
1065 snapDistanceSq *= snapDistanceSq;
1066
1067 for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
1068 if (predicate.test(n)
1069 && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
1070 nearestMap.computeIfAbsent(dist, k -> new LinkedList<>()).add(n);
1071 }
1072 }
1073 }
1074
1075 return nearestMap;
1076 }
1077
1078 /**
1079 * The *result* does not depend on the current map selection state,
1080 * neither does the result *order*.
1081 * It solely depends on the distance to point p.
1082 *
1083 * @param p the point for which to search the nearest segment.
1084 * @param ignore a collection of nodes which are not to be returned.
1085 * @param predicate the returned objects have to fulfill certain properties.
1086 *
1087 * @return All nodes nearest to point p that are in a belt from
1088 * dist(nearest) to dist(nearest)+4px around p and
1089 * that are not in ignore.
1090 */
1091 public final List<Node> getNearestNodes(Point p,
1092 Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
1093 List<Node> nearestList = Collections.emptyList();
1094
1095 if (ignore == null) {
1096 ignore = Collections.emptySet();
1097 }
1098
1099 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1100 if (!nlists.isEmpty()) {
1101 Double minDistSq = null;
1102 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1103 Double distSq = entry.getKey();
1104 List<Node> nlist = entry.getValue();
1105
1106 // filter nodes to be ignored before determining minDistSq..
1107 nlist.removeAll(ignore);
1108 if (minDistSq == null) {
1109 if (!nlist.isEmpty()) {
1110 minDistSq = distSq;
1111 nearestList = new ArrayList<>(nlist);
1112 }
1113 } else {
1114 if (distSq-minDistSq < 16) {
1115 nearestList.addAll(nlist);
1116 }
1117 }
1118 }
1119 }
1120
1121 return nearestList;
1122 }
1123
1124 /**
1125 * The *result* does not depend on the current map selection state,
1126 * neither does the result *order*.
1127 * It solely depends on the distance to point p.
1128 *
1129 * @param p the point for which to search the nearest segment.
1130 * @param predicate the returned objects have to fulfill certain properties.
1131 *
1132 * @return All nodes nearest to point p that are in a belt from
1133 * dist(nearest) to dist(nearest)+4px around p.
1134 * @see #getNearestNodes(Point, Collection, Predicate)
1135 */
1136 public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
1137 return getNearestNodes(p, null, predicate);
1138 }
1139
1140 /**
1141 * The *result* depends on the current map selection state IF use_selected is true.
1142 * <p>
1143 * If more than one node within node.snap-distance pixels is found,
1144 * the nearest node selected is returned IF use_selected is true.
1145 * <p>
1146 * Else the nearest new/id=0 node within about the same distance
1147 * as the true nearest node is returned.
1148 * <p>
1149 * If no such node is found either, the true nearest node to p is returned.
1150 * <p>
1151 * Finally, if a node is not found at all, {@code null} is returned.
1152 *
1153 * @param p the screen point
1154 * @param predicate this parameter imposes a condition on the returned object, e.g.
1155 * give the nearest node that is tagged.
1156 * @param useSelected make search depend on selection
1157 *
1158 * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1159 */
1160 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1161 return getNearestNode(p, predicate, useSelected, null);
1162 }
1163
1164 /**
1165 * The *result* depends on the current map selection state IF use_selected is true
1166 * <p>
1167 * If more than one node within node.snap-distance pixels is found,
1168 * the nearest node selected is returned IF use_selected is true.
1169 * <p>
1170 * If there are no selected nodes near that point, the node that is related to some of the preferredRefs
1171 * <p>
1172 * Else the nearest new/id=0 node within about the same distance
1173 * as the true nearest node is returned.
1174 * <p>
1175 * If no such node is found either, the true nearest node to p is returned.
1176 * <p>
1177 * Finally, if a node is not found at all, {@code null} is returned.
1178 *
1179 * @param p the screen point
1180 * @param predicate this parameter imposes a condition on the returned object, e.g.
1181 * give the nearest node that is tagged.
1182 * @param useSelected make search depend on selection
1183 * @param preferredRefs primitives, whose nodes we prefer
1184 *
1185 * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1186 * @since 6065
1187 */
1188 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
1189 boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1190
1191 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1192 if (nlists.isEmpty()) return null;
1193
1194 if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
1195 Node ntsel = null;
1196 Node ntnew = null;
1197 Node ntref = null;
1198 boolean useNtsel = useSelected;
1199 double minDistSq = nlists.keySet().iterator().next();
1200
1201 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1202 Double distSq = entry.getKey();
1203 for (Node nd : entry.getValue()) {
1204 // find the nearest selected node
1205 if (ntsel == null && nd.isSelected()) {
1206 ntsel = nd;
1207 // if there are multiple nearest nodes, prefer the one
1208 // that is selected. This is required in order to drag
1209 // the selected node if multiple nodes have the same
1210 // coordinates (e.g. after unglue)
1211 useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
1212 }
1213 if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
1214 List<OsmPrimitive> ndRefs = nd.getReferrers();
1215 if (preferredRefs.stream().anyMatch(ndRefs::contains)) {
1216 ntref = nd;
1217 }
1218 }
1219 // find the nearest newest node that is within about the same
1220 // distance as the true nearest node
1221 if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
1222 ntnew = nd;
1223 }
1224 }
1225 }
1226
1227 // take nearest selected, nearest new or true nearest node to p, in that order
1228 if (ntsel != null && useNtsel)
1229 return ntsel;
1230 if (ntref != null)
1231 return ntref;
1232 if (ntnew != null)
1233 return ntnew;
1234 return nlists.values().iterator().next().get(0);
1235 }
1236
1237 /**
1238 * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
1239 * @param p the screen point
1240 * @param predicate this parameter imposes a condition on the returned object, e.g.
1241 * give the nearest node that is tagged.
1242 *
1243 * @return The nearest node to point p.
1244 */
1245 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
1246 return getNearestNode(p, predicate, true);
1247 }
1248
1249 /**
1250 * The *result* does not depend on the current map selection state, neither does the result *order*.
1251 * It solely depends on the distance to point p.
1252 * @param p the screen point
1253 * @param predicate this parameter imposes a condition on the returned object, e.g.
1254 * give the nearest node that is tagged.
1255 *
1256 * @return a sorted map with the keys representing the perpendicular
1257 * distance of their associated way segments to point p.
1258 */
1259 private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
1260 Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
1261 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1262
1263 if (ds != null) {
1264 double snapDistanceSq = Config.getPref().getInt("mappaint.segment.snap-distance", 10);
1265 snapDistanceSq *= snapDistanceSq;
1266
1267 for (Way w : ds.searchWays(getBBox(p, Config.getPref().getInt("mappaint.segment.snap-distance", 10)))) {
1268 if (!predicate.test(w)) {
1269 continue;
1270 }
1271 Node lastN = null;
1272 int i = -2;
1273 for (Node n : w.getNodes()) {
1274 i++;
1275 if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
1276 continue;
1277 }
1278 if (lastN == null) {
1279 lastN = n;
1280 continue;
1281 }
1282
1283 Point2D pA = getPoint2D(lastN);
1284 Point2D pB = getPoint2D(n);
1285 double c = pA.distanceSq(pB);
1286 double a = p.distanceSq(pB);
1287 double b = p.distanceSq(pA);
1288
1289 /* perpendicular distance squared
1290 * loose some precision to account for possible deviations in the calculation above
1291 * e.g. if identical (A and B) come about reversed in another way, values may differ
1292 * -- zero out least significant 32 dual digits of mantissa.
1293 */
1294 double perDistSq = Double.longBitsToDouble(
1295 Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c)
1296 >> 32 << 32); // resolution in numbers with large exponent not needed here.
1297
1298 if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
1299 nearestMap.computeIfAbsent(perDistSq, k -> new LinkedList<>()).add(new WaySegment(w, i));
1300 }
1301
1302 lastN = n;
1303 }
1304 }
1305 }
1306
1307 return nearestMap;
1308 }
1309
1310 /**
1311 * The result *order* depends on the current map selection state.
1312 * Segments within 10px of p are searched and sorted by their distance to {@code p},
1313 * then, within groups of equally distant segments, prefer those that are selected.
1314 *
1315 * @param p the point for which to search the nearest segments.
1316 * @param ignore a collection of segments which are not to be returned.
1317 * @param predicate the returned objects have to fulfill certain properties.
1318 *
1319 * @return all segments within 10px of p that are not in ignore,
1320 * sorted by their perpendicular distance.
1321 */
1322 public final List<WaySegment> getNearestWaySegments(Point p,
1323 Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
1324 List<WaySegment> nearestList = new ArrayList<>();
1325 List<WaySegment> unselected = new LinkedList<>();
1326
1327 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1328 // put selected waysegs within each distance group first
1329 // makes the order of nearestList dependent on current selection state
1330 for (WaySegment ws : wss) {
1331 (ws.getWay().isSelected() ? nearestList : unselected).add(ws);
1332 }
1333 nearestList.addAll(unselected);
1334 unselected.clear();
1335 }
1336 if (ignore != null) {
1337 nearestList.removeAll(ignore);
1338 }
1339
1340 return nearestList;
1341 }
1342
1343 /**
1344 * The result *order* depends on the current map selection state.
1345 *
1346 * @param p the point for which to search the nearest segments.
1347 * @param predicate the returned objects have to fulfill certain properties.
1348 *
1349 * @return all segments within 10px of p, sorted by their perpendicular distance.
1350 * @see #getNearestWaySegments(Point, Collection, Predicate)
1351 */
1352 public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
1353 return getNearestWaySegments(p, null, predicate);
1354 }
1355
1356 /**
1357 * The *result* depends on the current map selection state IF use_selected is true.
1358 *
1359 * @param p the point for which to search the nearest segment.
1360 * @param predicate the returned object has to fulfill certain properties.
1361 * @param useSelected whether selected way segments should be preferred.
1362 *
1363 * @return The nearest way segment to point p,
1364 * and, depending on use_selected, prefers a selected way segment, if found.
1365 * @see #getNearestWaySegments(Point, Collection, Predicate)
1366 */
1367 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1368 WaySegment wayseg = null;
1369 WaySegment ntsel = null;
1370
1371 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1372 if (wayseg != null && ntsel != null) {
1373 break;
1374 }
1375 for (WaySegment ws : wslist) {
1376 if (wayseg == null) {
1377 wayseg = ws;
1378 }
1379 if (ntsel == null && ws.getWay().isSelected()) {
1380 ntsel = ws;
1381 }
1382 }
1383 }
1384
1385 return (ntsel != null && useSelected) ? ntsel : wayseg;
1386 }
1387
1388 /**
1389 * The *result* depends on the current map selection state IF use_selected is true.
1390 *
1391 * @param p the point for which to search the nearest segment.
1392 * @param predicate the returned object has to fulfill certain properties.
1393 * @param useSelected whether selected way segments should be preferred.
1394 * @param preferredRefs - prefer segments related to these primitives, may be null
1395 *
1396 * @return The nearest way segment to point p,
1397 * and, depending on use_selected, prefers a selected way segment, if found.
1398 * Also prefers segments of ways that are related to one of preferredRefs primitives
1399 *
1400 * @see #getNearestWaySegments(Point, Collection, Predicate)
1401 * @since 6065
1402 */
1403 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
1404 boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1405 WaySegment wayseg = null;
1406 if (preferredRefs != null && preferredRefs.isEmpty())
1407 preferredRefs = null;
1408
1409 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1410 for (WaySegment ws : wslist) {
1411 if (wayseg == null) {
1412 wayseg = ws;
1413 }
1414 if (useSelected && ws.getWay().isSelected()) {
1415 return ws;
1416 }
1417 if (!Utils.isEmpty(preferredRefs)) {
1418 // prefer ways containing given nodes
1419 if (preferredRefs.contains(ws.getFirstNode()) || preferredRefs.contains(ws.getSecondNode())) {
1420 return ws;
1421 }
1422 Collection<OsmPrimitive> wayRefs = ws.getWay().getReferrers();
1423 // prefer member of the given relations
1424 for (OsmPrimitive ref: preferredRefs) {
1425 if (ref instanceof Relation && wayRefs.contains(ref)) {
1426 return ws;
1427 }
1428 }
1429 }
1430 }
1431 }
1432 return wayseg;
1433 }
1434
1435 /**
1436 * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
1437 * @param p the point for which to search the nearest segment.
1438 * @param predicate the returned object has to fulfill certain properties.
1439 *
1440 * @return The nearest way segment to point p.
1441 */
1442 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
1443 return getNearestWaySegment(p, predicate, true);
1444 }
1445
1446 /**
1447 * The *result* does not depend on the current map selection state,
1448 * neither does the result *order*.
1449 * It solely depends on the perpendicular distance to point p.
1450 *
1451 * @param p the point for which to search the nearest ways.
1452 * @param ignore a collection of ways which are not to be returned.
1453 * @param predicate the returned object has to fulfill certain properties.
1454 *
1455 * @return all nearest ways to the screen point given that are not in ignore.
1456 * @see #getNearestWaySegments(Point, Collection, Predicate)
1457 */
1458 public final List<Way> getNearestWays(Point p,
1459 Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
1460 Set<Way> wset = new HashSet<>();
1461
1462 List<Way> nearestList = getNearestWaySegmentsImpl(p, predicate).values().stream()
1463 .flatMap(Collection::stream)
1464 .filter(ws -> wset.add(ws.getWay()))
1465 .map(IWaySegment::getWay)
1466 .collect(Collectors.toList());
1467 if (ignore != null) {
1468 nearestList.removeAll(ignore);
1469 }
1470
1471 return nearestList;
1472 }
1473
1474 /**
1475 * The *result* does not depend on the current map selection state,
1476 * neither does the result *order*.
1477 * It solely depends on the perpendicular distance to point p.
1478 *
1479 * @param p the point for which to search the nearest ways.
1480 * @param predicate the returned object has to fulfill certain properties.
1481 *
1482 * @return all nearest ways to the screen point given.
1483 * @see #getNearestWays(Point, Collection, Predicate)
1484 */
1485 public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1486 return getNearestWays(p, null, predicate);
1487 }
1488
1489 /**
1490 * The *result* depends on the current map selection state.
1491 *
1492 * @param p the point for which to search the nearest segment.
1493 * @param predicate the returned object has to fulfill certain properties.
1494 *
1495 * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
1496 * @see #getNearestWaySegment(Point, Predicate)
1497 */
1498 public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
1499 WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
1500 return (nearestWaySeg == null) ? null : nearestWaySeg.getWay();
1501 }
1502
1503 /**
1504 * The *result* does not depend on the current map selection state,
1505 * neither does the result *order*.
1506 * It solely depends on the distance to point p.
1507 * <p>
1508 * First, nodes will be searched. If there are nodes within BBox found,
1509 * return a collection of those nodes only.
1510 * <p>
1511 * If no nodes are found, search for nearest ways. If there are ways
1512 * within BBox found, return a collection of those ways only.
1513 * <p>
1514 * If nothing is found, return an empty collection.
1515 *
1516 * @param p The point on screen.
1517 * @param ignore a collection of ways which are not to be returned.
1518 * @param predicate the returned object has to fulfill certain properties.
1519 *
1520 * @return Primitives nearest to the given screen point that are not in ignore.
1521 * @see #getNearestNodes(Point, Collection, Predicate)
1522 * @see #getNearestWays(Point, Collection, Predicate)
1523 */
1524 public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
1525 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1526 List<OsmPrimitive> nearestList = Collections.emptyList();
1527 OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
1528
1529 if (osm != null) {
1530 if (osm instanceof Node) {
1531 nearestList = new ArrayList<>(getNearestNodes(p, predicate));
1532 } else if (osm instanceof Way) {
1533 nearestList = new ArrayList<>(getNearestWays(p, predicate));
1534 }
1535 if (ignore != null) {
1536 nearestList.removeAll(ignore);
1537 }
1538 }
1539
1540 return nearestList;
1541 }
1542
1543 /**
1544 * The *result* does not depend on the current map selection state,
1545 * neither does the result *order*.
1546 * It solely depends on the distance to point p.
1547 *
1548 * @param p The point on screen.
1549 * @param predicate the returned object has to fulfill certain properties.
1550 * @return Primitives nearest to the given screen point.
1551 * @see #getNearestNodesOrWays(Point, Collection, Predicate)
1552 */
1553 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1554 return getNearestNodesOrWays(p, null, predicate);
1555 }
1556
1557 /**
1558 * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
1559 * It decides, whether to yield the node to be tested or look for further (way) candidates.
1560 *
1561 * @param osm node to check
1562 * @param p point clicked
1563 * @param useSelected whether to prefer selected nodes
1564 * @return true, if the node fulfills the properties of the function body
1565 */
1566 private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
1567 if (osm != null) {
1568 if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
1569 if (osm.isTagged()) return true;
1570 if (useSelected && osm.isSelected()) return true;
1571 }
1572 return false;
1573 }
1574
1575 /**
1576 * The *result* depends on the current map selection state IF use_selected is true.
1577 * <p>
1578 * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
1579 * the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)}
1580 * to find the nearest selected way.
1581 * <p>
1582 * IF use_selected is false, or if no selected primitive was found, do the following.
1583 * <p>
1584 * If the nearest node found is within 4px of p, simply take it.
1585 * Else, find the nearest way segment. Then, if p is closer to its
1586 * middle than to the node, take the way segment, else take the node.
1587 * <p>
1588 * Finally, if no nearest primitive is found at all, return {@code null}.
1589 *
1590 * @param p The point on screen.
1591 * @param predicate the returned object has to fulfill certain properties.
1592 * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
1593 *
1594 * @return A primitive within snap-distance to point p,
1595 * that is chosen by the algorithm described.
1596 * @see #getNearestNode(Point, Predicate)
1597 * @see #getNearestWay(Point, Predicate)
1598 */
1599 public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1600 Collection<OsmPrimitive> sel;
1601 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1602 if (useSelected && ds != null) {
1603 sel = ds.getSelected();
1604 } else {
1605 sel = null;
1606 }
1607 OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
1608
1609 if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
1610 WaySegment ws;
1611 if (useSelected) {
1612 ws = getNearestWaySegment(p, predicate, useSelected, sel);
1613 } else {
1614 ws = getNearestWaySegment(p, predicate, useSelected);
1615 }
1616 if (ws == null) return osm;
1617
1618 if ((ws.getWay().isSelected() && useSelected) || osm == null) {
1619 // either (no _selected_ nearest node found, if desired) or no nearest node was found
1620 osm = ws.getWay();
1621 } else {
1622 int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1623 maxWaySegLenSq *= maxWaySegLenSq;
1624
1625 Point2D wp1 = getPoint2D(ws.getFirstNode());
1626 Point2D wp2 = getPoint2D(ws.getSecondNode());
1627
1628 // is wayseg shorter than maxWaySegLenSq and
1629 // is p closer to the middle of wayseg than to the nearest node?
1630 if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1631 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
1632 osm = ws.getWay();
1633 }
1634 }
1635 return osm;
1636 }
1637
1638 /**
1639 * if r = 0 returns a, if r=1 returns b,
1640 * if r = 0.5 returns center between a and b, etc.
1641 *
1642 * @param r scale value
1643 * @param a root of vector
1644 * @param b vector
1645 * @return new point at a + r*(ab)
1646 */
1647 public static Point2D project(double r, Point2D a, Point2D b) {
1648 Point2D ret = null;
1649
1650 if (a != null && b != null) {
1651 ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1652 a.getY() + r*(b.getY()-a.getY()));
1653 }
1654 return ret;
1655 }
1656
1657 /**
1658 * The *result* does not depend on the current map selection state, neither does the result *order*.
1659 * It solely depends on the distance to point p.
1660 *
1661 * @param p The point on screen.
1662 * @param ignore a collection of ways which are not to be returned.
1663 * @param predicate the returned object has to fulfill certain properties.
1664 *
1665 * @return a list of all objects that are nearest to point p and
1666 * not in ignore or an empty list if nothing was found.
1667 */
1668 public final List<OsmPrimitive> getAllNearest(Point p,
1669 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1670 Set<Way> wset = new HashSet<>();
1671
1672 // add nearby ways
1673 List<OsmPrimitive> nearestList = getNearestWaySegmentsImpl(p, predicate).values().stream()
1674 .flatMap(Collection::stream)
1675 .filter(ws -> wset.add(ws.getWay()))
1676 .map(IWaySegment::getWay)
1677 .collect(Collectors.toList());
1678
1679 // add nearby nodes
1680 getNearestNodesImpl(p, predicate).values()
1681 .forEach(nearestList::addAll);
1682
1683 // add parent relations of nearby nodes and ways
1684 Set<OsmPrimitive> parentRelations = nearestList.stream()
1685 .flatMap(o -> o.referrers(Relation.class))
1686 .filter(predicate)
1687 .collect(Collectors.toSet());
1688 nearestList.addAll(parentRelations);
1689
1690 if (ignore != null) {
1691 nearestList.removeAll(ignore);
1692 }
1693
1694 return nearestList;
1695 }
1696
1697 /**
1698 * The *result* does not depend on the current map selection state, neither does the result *order*.
1699 * It solely depends on the distance to point p.
1700 *
1701 * @param p The point on screen.
1702 * @param predicate the returned object has to fulfill certain properties.
1703 *
1704 * @return a list of all objects that are nearest to point p
1705 * or an empty list if nothing was found.
1706 * @see #getAllNearest(Point, Collection, Predicate)
1707 */
1708 public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1709 return getAllNearest(p, null, predicate);
1710 }
1711
1712 /**
1713 * Returns the projection to be used in calculating stuff.
1714 * @return The projection to be used in calculating stuff.
1715 */
1716 public Projection getProjection() {
1717 return state.getProjection();
1718 }
1719
1720 @Override
1721 public String helpTopic() {
1722 String n = getClass().getName();
1723 return n.substring(n.lastIndexOf('.')+1);
1724 }
1725
1726 /**
1727 * Return an ID which is unique as long as viewport dimensions are the same
1728 * @return A unique ID, as long as viewport dimensions are the same
1729 */
1730 public int getViewID() {
1731 EastNorth center = getCenter();
1732 String x = String.valueOf(center.east()) +
1733 '_' + center.north() +
1734 '_' + getScale() +
1735 '_' + getWidth() +
1736 '_' + getHeight() +
1737 '_' + getProjection();
1738 CRC32 id = new CRC32();
1739 id.update(x.getBytes(StandardCharsets.UTF_8));
1740 return (int) id.getValue();
1741 }
1742
1743 /**
1744 * Set new cursor.
1745 * @param cursor The new cursor to use.
1746 * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1747 */
1748 public void setNewCursor(Cursor cursor, Object reference) {
1749 cursorManager.setNewCursor(cursor, reference);
1750 }
1751
1752 /**
1753 * Set new cursor.
1754 * @param cursor the type of predefined cursor
1755 * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1756 */
1757 public void setNewCursor(int cursor, Object reference) {
1758 setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1759 }
1760
1761 /**
1762 * Remove the new cursor and reset to previous
1763 * @param reference Cursor reference
1764 */
1765 public void resetCursor(Object reference) {
1766 cursorManager.resetCursor(reference);
1767 }
1768
1769 /**
1770 * Gets the cursor manager that is used for this NavigatableComponent.
1771 * @return The cursor manager.
1772 */
1773 public CursorManager getCursorManager() {
1774 return cursorManager;
1775 }
1776
1777 /**
1778 * Get a max scale for projection that describes world in 1/512 of the projection unit
1779 * @return max scale
1780 */
1781 public double getMaxScale() {
1782 ProjectionBounds world = getMaxProjectionBounds();
1783 return Math.max(
1784 world.maxNorth-world.minNorth,
1785 world.maxEast-world.minEast
1786 )/512;
1787 }
1788
1789 /**
1790 * Listener for mouse movement events. Used to detect when primitives are being hovered over with the mouse pointer
1791 * so that registered {@link PrimitiveHoverListener}s can be notified.
1792 */
1793 private final class PrimitiveHoverMouseListener extends MouseAdapter {
1794 @Override
1795 public void mouseMoved(MouseEvent e) {
1796 OsmPrimitive hovered = getNearestNodeOrWay(e.getPoint(), isSelectablePredicate, true);
1797 updateHoveredPrimitive(hovered, e);
1798 }
1799
1800 @Override
1801 public void mouseExited(MouseEvent e) {
1802 updateHoveredPrimitive(null, e);
1803 }
1804 }
1805}
Note: See TracBrowser for help on using the repository browser.