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

Last change on this file since 16553 was 16553, checked in by Don-vip, 4 years ago

see #19334 - javadoc fixes + protected constructors for abstract classes

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