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, 4 weeks 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.