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

Last change on this file since 16225 was 15767, checked in by GerdP, 5 years ago

fix #18623: Zoom is too close when downloading a single node
Don't zoom to viewport bounds with no extend.

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