001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import java.awt.Cursor;
005import java.awt.Point;
006import java.awt.Rectangle;
007import java.awt.event.ComponentAdapter;
008import java.awt.event.ComponentEvent;
009import java.awt.event.HierarchyEvent;
010import java.awt.event.HierarchyListener;
011import java.awt.geom.AffineTransform;
012import java.awt.geom.Point2D;
013import java.nio.charset.StandardCharsets;
014import java.text.NumberFormat;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.Collections;
018import java.util.HashSet;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.Set;
024import java.util.Stack;
025import java.util.TreeMap;
026import java.util.concurrent.CopyOnWriteArrayList;
027import java.util.function.Predicate;
028import java.util.stream.Collectors;
029import java.util.zip.CRC32;
030
031import javax.swing.JComponent;
032import javax.swing.SwingUtilities;
033
034import org.openstreetmap.josm.data.Bounds;
035import org.openstreetmap.josm.data.ProjectionBounds;
036import org.openstreetmap.josm.data.SystemOfMeasurement;
037import org.openstreetmap.josm.data.ViewportData;
038import org.openstreetmap.josm.data.coor.EastNorth;
039import org.openstreetmap.josm.data.coor.ILatLon;
040import org.openstreetmap.josm.data.coor.LatLon;
041import org.openstreetmap.josm.data.osm.BBox;
042import org.openstreetmap.josm.data.osm.DataSet;
043import org.openstreetmap.josm.data.osm.Node;
044import org.openstreetmap.josm.data.osm.OsmPrimitive;
045import org.openstreetmap.josm.data.osm.Relation;
046import org.openstreetmap.josm.data.osm.Way;
047import org.openstreetmap.josm.data.osm.WaySegment;
048import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
049import org.openstreetmap.josm.data.preferences.BooleanProperty;
050import org.openstreetmap.josm.data.preferences.DoubleProperty;
051import org.openstreetmap.josm.data.preferences.IntegerProperty;
052import org.openstreetmap.josm.data.projection.Projection;
053import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
054import org.openstreetmap.josm.data.projection.ProjectionRegistry;
055import org.openstreetmap.josm.gui.help.Helpful;
056import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
057import org.openstreetmap.josm.gui.layer.NativeScaleLayer.Scale;
058import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
059import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
060import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
061import org.openstreetmap.josm.gui.util.CursorManager;
062import org.openstreetmap.josm.gui.util.GuiHelper;
063import org.openstreetmap.josm.spi.preferences.Config;
064import org.openstreetmap.josm.tools.Logging;
065import org.openstreetmap.josm.tools.Utils;
066
067/**
068 * A component that can be navigated by a {@link MapMover}. Used as map view and for the
069 * zoomer in the download dialog.
070 *
071 * @author imi
072 * @since 41
073 */
074public class NavigatableComponent extends JComponent implements Helpful {
075
076    private static final double ALIGNMENT_EPSILON = 1e-3;
077
078    /**
079     * Interface to notify listeners of the change of the zoom area.
080     * @since 10600 (functional interface)
081     */
082    @FunctionalInterface
083    public interface ZoomChangeListener {
084        /**
085         * Method called when the zoom area has changed.
086         */
087        void zoomChanged();
088    }
089
090    /**
091     * To determine if a primitive is currently selectable.
092     */
093    public transient Predicate<OsmPrimitive> isSelectablePredicate = prim -> {
094        if (!prim.isSelectable()) return false;
095        // if it isn't displayed on screen, you cannot click on it
096        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
097        try {
098            return !MapPaintStyles.getStyles().get(prim, getDist100Pixel(), this).isEmpty();
099        } 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     * Returns geographic coordinates from a specific pixel coordination on the screen.
433     * @param x X-Pixelposition to get coordinate from
434     * @param y Y-Pixelposition to get coordinate from
435     *
436     * @return Geographic coordinates from a specific pixel coordination on the screen.
437     */
438    public EastNorth getEastNorth(int x, int y) {
439        return state.getForView(x, y).getEastNorth();
440    }
441
442    /**
443     * Determines the projection bounds of view area.
444     * @return the projection bounds of view area
445     */
446    public ProjectionBounds getProjectionBounds() {
447        return getState().getViewArea().getProjectionBounds();
448    }
449
450    /* FIXME: replace with better method - used by MapSlider */
451    public ProjectionBounds getMaxProjectionBounds() {
452        Bounds b = getProjection().getWorldBoundsLatLon();
453        return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
454                getProjection().latlon2eastNorth(b.getMax()));
455    }
456
457    /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
458    public Bounds getRealBounds() {
459        return getState().getViewArea().getCornerBounds();
460    }
461
462    /**
463     * Returns unprojected geographic coordinates for a specific pixel position on the screen.
464     * @param x X-Pixelposition to get coordinate from
465     * @param y Y-Pixelposition to get coordinate from
466     *
467     * @return Geographic unprojected coordinates from a specific pixel position on the screen.
468     */
469    public LatLon getLatLon(int x, int y) {
470        return getProjection().eastNorth2latlon(getEastNorth(x, y));
471    }
472
473    /**
474     * Returns unprojected geographic coordinates for a specific pixel position on the screen.
475     * @param x X-Pixelposition to get coordinate from
476     * @param y Y-Pixelposition to get coordinate from
477     *
478     * @return Geographic unprojected coordinates from a specific pixel position on the screen.
479     */
480    public LatLon getLatLon(double x, double y) {
481        return getLatLon((int) x, (int) y);
482    }
483
484    /**
485     * Determines the projection bounds of given rectangle.
486     * @param r rectangle
487     * @return the projection bounds of {@code r}
488     */
489    public ProjectionBounds getProjectionBounds(Rectangle r) {
490        return getState().getViewArea(r).getProjectionBounds();
491    }
492
493    /**
494     * Returns minimum bounds that will cover a given rectangle.
495     * @param r rectangle
496     * @return Minimum bounds that will cover rectangle
497     */
498    public Bounds getLatLonBounds(Rectangle r) {
499        return ProjectionRegistry.getProjection().getLatLonBoundsBox(getProjectionBounds(r));
500    }
501
502    /**
503     * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
504     * @return The affine transform.
505     */
506    public AffineTransform getAffineTransform() {
507        return getState().getAffineTransform();
508    }
509
510    /**
511     * Return the point on the screen where this Coordinate would be.
512     * @param p The point, where this geopoint would be drawn.
513     * @return The point on screen where "point" would be drawn, relative to the own top/left.
514     */
515    public Point2D getPoint2D(EastNorth p) {
516        if (null == p)
517            return new Point();
518        return getState().getPointFor(p).getInView();
519    }
520
521    /**
522     * Return the point on the screen where this Coordinate would be.
523     *
524     * Alternative: {@link #getState()}, then {@link MapViewState#getPointFor(ILatLon)}
525     * @param latlon The point, where this geopoint would be drawn.
526     * @return The point on screen where "point" would be drawn, relative to the own top/left.
527     */
528    public Point2D getPoint2D(ILatLon latlon) {
529        if (latlon == null) {
530            return new Point();
531        } else {
532            return getPoint2D(latlon.getEastNorth(ProjectionRegistry.getProjection()));
533        }
534    }
535
536    /**
537     * Return the point on the screen where this Coordinate would be.
538     *
539     * Alternative: {@link #getState()}, then {@link MapViewState#getPointFor(ILatLon)}
540     * @param latlon The point, where this geopoint would be drawn.
541     * @return The point on screen where "point" would be drawn, relative to the own top/left.
542     */
543    public Point2D getPoint2D(LatLon latlon) {
544        return getPoint2D((ILatLon) latlon);
545    }
546
547    /**
548     * Return the point on the screen where this Node would be.
549     *
550     * Alternative: {@link #getState()}, then {@link MapViewState#getPointFor(ILatLon)}
551     * @param n The node, where this geopoint would be drawn.
552     * @return The point on screen where "node" would be drawn, relative to the own top/left.
553     */
554    public Point2D getPoint2D(Node n) {
555        return getPoint2D(n.getEastNorth());
556    }
557
558    /**
559     * looses precision, may overflow (depends on p and current scale)
560     * @param p east/north
561     * @return point
562     * @see #getPoint2D(EastNorth)
563     */
564    public Point getPoint(EastNorth p) {
565        Point2D d = getPoint2D(p);
566        return new Point((int) d.getX(), (int) d.getY());
567    }
568
569    /**
570     * looses precision, may overflow (depends on p and current scale)
571     * @param latlon lat/lon
572     * @return point
573     * @see #getPoint2D(LatLon)
574     * @since 12725
575     */
576    public Point getPoint(ILatLon latlon) {
577        Point2D d = getPoint2D(latlon);
578        return new Point((int) d.getX(), (int) d.getY());
579    }
580
581    /**
582     * looses precision, may overflow (depends on p and current scale)
583     * @param latlon lat/lon
584     * @return point
585     * @see #getPoint2D(LatLon)
586     */
587    public Point getPoint(LatLon latlon) {
588        return getPoint((ILatLon) latlon);
589    }
590
591    /**
592     * looses precision, may overflow (depends on p and current scale)
593     * @param n node
594     * @return point
595     * @see #getPoint2D(Node)
596     */
597    public Point getPoint(Node n) {
598        Point2D d = getPoint2D(n);
599        return new Point((int) d.getX(), (int) d.getY());
600    }
601
602    /**
603     * Zoom to the given coordinate and scale.
604     *
605     * @param newCenter The center x-value (easting) to zoom to.
606     * @param newScale The scale to use.
607     */
608    public void zoomTo(EastNorth newCenter, double newScale) {
609        zoomTo(newCenter, newScale, false);
610    }
611
612    /**
613     * Zoom to the given coordinate and scale.
614     *
615     * @param center The center x-value (easting) to zoom to.
616     * @param scale The scale to use.
617     * @param initial true if this call initializes the viewport.
618     */
619    public void zoomTo(EastNorth center, double scale, boolean initial) {
620        Bounds b = getProjection().getWorldBoundsLatLon();
621        ProjectionBounds pb = getProjection().getWorldBoundsBoxEastNorth();
622        double newScale = scale;
623        int width = getWidth();
624        int height = getHeight();
625
626        // make sure, the center of the screen is within projection bounds
627        double east = center.east();
628        double north = center.north();
629        east = Math.max(east, pb.minEast);
630        east = Math.min(east, pb.maxEast);
631        north = Math.max(north, pb.minNorth);
632        north = Math.min(north, pb.maxNorth);
633        EastNorth newCenter = new EastNorth(east, north);
634
635        // don't zoom out too much, the world bounds should be at least
636        // half the size of the screen
637        double pbHeight = pb.maxNorth - pb.minNorth;
638        if (height > 0 && 2 * pbHeight < height * newScale) {
639            double newScaleH = 2 * pbHeight / height;
640            double pbWidth = pb.maxEast - pb.minEast;
641            if (width > 0 && 2 * pbWidth < width * newScale) {
642                double newScaleW = 2 * pbWidth / width;
643                newScale = Math.max(newScaleH, newScaleW);
644            }
645        }
646
647        // don't zoom in too much, minimum: 100 px = 1 cm
648        LatLon ll1 = getLatLon(width / 2 - 50, height / 2);
649        LatLon ll2 = getLatLon(width / 2 + 50, height / 2);
650        if (ll1.isValid() && ll2.isValid() && b.contains(ll1) && b.contains(ll2)) {
651            double dm = ll1.greatCircleDistance(ll2);
652            double den = 100 * getScale();
653            double scaleMin = 0.01 * den / dm / 100;
654            if (newScale < scaleMin && !Double.isInfinite(scaleMin)) {
655                newScale = scaleMin;
656            }
657        }
658
659        // snap scale to imagery if needed
660        newScale = scaleRound(newScale);
661
662        // Align to the pixel grid:
663        // This is a sub-pixel correction to ensure consistent drawing at a certain scale.
664        // For example take 2 nodes, that have a distance of exactly 2.6 pixels.
665        // Depending on the offset, the distance in rounded or truncated integer
666        // pixels will be 2 or 3. It is preferable to have a consistent distance
667        // and not switch back and forth as the viewport moves. This can be achieved by
668        // locking an arbitrary point to integer pixel coordinates. (Here the EastNorth
669        // origin is used as reference point.)
670        // Note that the normal right mouse button drag moves the map by integer pixel
671        // values, so it is not an issue in this case. It only shows when zooming
672        // in & back out, etc.
673        MapViewState mvs = getState().usingScale(newScale);
674        mvs = mvs.movedTo(mvs.getCenter(), newCenter);
675        Point2D enOrigin = mvs.getPointFor(new EastNorth(0, 0)).getInView();
676        // as a result of the alignment, it is common to round "half integer" values
677        // like 1.49999, which is numerically unstable; add small epsilon to resolve this
678        Point2D enOriginAligned = new Point2D.Double(
679                Math.round(enOrigin.getX()) + ALIGNMENT_EPSILON,
680                Math.round(enOrigin.getY()) + ALIGNMENT_EPSILON);
681        EastNorth enShift = mvs.getForView(enOriginAligned.getX(), enOriginAligned.getY()).getEastNorth();
682        newCenter = newCenter.subtract(enShift);
683
684        EastNorth oldCenter = getCenter();
685        if (!newCenter.equals(oldCenter) || !Utils.equalsEpsilon(getScale(), newScale)) {
686            if (!initial) {
687                pushZoomUndo(oldCenter, getScale());
688            }
689            zoomNoUndoTo(newCenter, newScale, initial);
690        }
691    }
692
693    /**
694     * Zoom to the given coordinate without adding to the zoom undo buffer.
695     *
696     * @param newCenter The center x-value (easting) to zoom to.
697     * @param newScale The scale to use.
698     * @param initial true if this call initializes the viewport.
699     */
700    private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) {
701        if (!Utils.equalsEpsilon(getScale(), newScale)) {
702            state = state.usingScale(newScale);
703        }
704        if (!newCenter.equals(getCenter())) {
705            state = state.movedTo(state.getCenter(), newCenter);
706        }
707        if (!initial) {
708            repaint();
709            fireZoomChanged();
710        }
711    }
712
713    /**
714     * Zoom to given east/north.
715     * @param newCenter new center coordinates
716     */
717    public void zoomTo(EastNorth newCenter) {
718        zoomTo(newCenter, getScale());
719    }
720
721    /**
722     * Zoom to given lat/lon.
723     * @param newCenter new center coordinates
724     * @since 12725
725     */
726    public void zoomTo(ILatLon newCenter) {
727        zoomTo(getProjection().latlon2eastNorth(newCenter));
728    }
729
730    /**
731     * Zoom to given lat/lon.
732     * @param newCenter new center coordinates
733     */
734    public void zoomTo(LatLon newCenter) {
735        zoomTo((ILatLon) newCenter);
736    }
737
738    /**
739     * Thread class for smooth scrolling. Made a separate class, so we can safely terminate it.
740     */
741    private class SmoothScrollThread extends Thread {
742        private boolean doStop;
743        private final EastNorth oldCenter = getCenter();
744        private final EastNorth finalNewCenter;
745        private final long frames;
746        private final long sleepTime;
747
748        SmoothScrollThread(EastNorth newCenter, long frameNum, int fps) {
749            super("smooth-scroller");
750            finalNewCenter = newCenter;
751            frames = frameNum;
752            sleepTime = 1000L / fps;
753        }
754
755        @Override
756        public void run() {
757            try {
758                for (int i = 0; i < frames && !doStop; i++) {
759                    final EastNorth z = oldCenter.interpolate(finalNewCenter, (1.0+i) / frames);
760                    GuiHelper.runInEDTAndWait(() -> {
761                        zoomTo(z);
762                    });
763                    Thread.sleep(sleepTime);
764                }
765            } catch (InterruptedException ex) {
766                Logging.warn("Interruption during smooth scrolling");
767            }
768        }
769
770        public void stopIt() {
771            doStop = true;
772        }
773    }
774
775    /**
776     * Create a thread that moves the viewport to the given center in an animated fashion.
777     * @param newCenter new east/north center
778     */
779    public void smoothScrollTo(EastNorth newCenter) {
780        final EastNorth oldCenter = getCenter();
781        if (!newCenter.equals(oldCenter)) {
782            final int fps = Config.getPref().getInt("smooth.scroll.fps", 20);     // animation frames per second
783            final int speed = Config.getPref().getInt("smooth.scroll.speed", 1500); // milliseconds for full-screen-width pan
784            final int maxtime = Config.getPref().getInt("smooth.scroll.maxtime", 5000); // milliseconds maximum scroll time
785            final double distance = newCenter.distance(oldCenter) / getScale();
786            double milliseconds = distance / getWidth() * speed;
787            if (milliseconds > maxtime) { // prevent overlong scroll time, speed up if necessary
788                milliseconds = maxtime;
789            }
790
791            ThreadGroup group = Thread.currentThread().getThreadGroup();
792            Thread[] threads = new Thread[group.activeCount()];
793            group.enumerate(threads, true);
794            boolean stopped = false;
795            for (Thread t : threads) {
796                if (t instanceof SmoothScrollThread) {
797                    ((SmoothScrollThread) t).stopIt();
798                    /* handle this case outside in case there is more than one smooth thread */
799                    stopped = true;
800                }
801            }
802            if (stopped && milliseconds > maxtime/2.0) { /* we aren't fast enough, skip smooth */
803                Logging.warn("Skip smooth scrolling");
804                zoomTo(newCenter);
805            } else {
806                long frames = Math.round(milliseconds * fps / 1000);
807                if (frames <= 1)
808                    zoomTo(newCenter);
809                else
810                    new SmoothScrollThread(newCenter, frames, fps).start();
811            }
812        }
813    }
814
815    public void zoomManyTimes(double x, double y, int times) {
816        double oldScale = getScale();
817        double newScale = scaleZoomManyTimes(times);
818        zoomToFactor(x, y, newScale / oldScale);
819    }
820
821    public void zoomToFactor(double x, double y, double factor) {
822        double newScale = getScale()*factor;
823        EastNorth oldUnderMouse = getState().getForView(x, y).getEastNorth();
824        MapViewState newState = getState().usingScale(newScale);
825        newState = newState.movedTo(newState.getForView(x, y), oldUnderMouse);
826        zoomTo(newState.getCenter().getEastNorth(), newScale);
827    }
828
829    public void zoomToFactor(EastNorth newCenter, double factor) {
830        zoomTo(newCenter, getScale()*factor);
831    }
832
833    public void zoomToFactor(double factor) {
834        zoomTo(getCenter(), getScale()*factor);
835    }
836
837    /**
838     * Zoom to given projection bounds.
839     * @param box new projection bounds
840     */
841    public void zoomTo(ProjectionBounds box) {
842        double newScale = box.getScale(getWidth(), getHeight());
843        newScale = scaleFloor(newScale);
844        zoomTo(box.getCenter(), newScale);
845    }
846
847    /**
848     * Zoom to given bounds.
849     * @param box new bounds
850     */
851    public void zoomTo(Bounds box) {
852        zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
853                getProjection().latlon2eastNorth(box.getMax())));
854    }
855
856    /**
857     * Zoom to given viewport data.
858     * @param viewport new viewport data
859     */
860    public void zoomTo(ViewportData viewport) {
861        if (viewport == null) return;
862        if (viewport.getBounds() != null) {
863            if (!viewport.getBounds().hasExtend()) {
864                // see #18623
865                BoundingXYVisitor v = new BoundingXYVisitor();
866                v.visit(viewport.getBounds());
867                zoomTo(v);
868            } else {
869                zoomTo(viewport.getBounds());
870            }
871
872        } else {
873            zoomTo(viewport.getCenter(), viewport.getScale(), true);
874        }
875    }
876
877    /**
878     * Set the new dimension to the view.
879     * @param v box to zoom to
880     */
881    public void zoomTo(BoundingXYVisitor v) {
882        if (v == null) {
883            v = new BoundingXYVisitor();
884        }
885        if (v.getBounds() == null) {
886            v.visit(getProjection().getWorldBoundsLatLon());
887        }
888
889        // increase bbox. This is required
890        // especially if the bbox contains one single node, but helpful
891        // in most other cases as well.
892        // Do not zoom if the current scale covers the selection, #16706
893        final MapView mapView = MainApplication.getMap().mapView;
894        final double mapScale = mapView.getScale();
895        final double minScale = v.getBounds().getScale(mapView.getWidth(), mapView.getHeight());
896        v.enlargeBoundingBoxLogarithmically();
897        final double maxScale = v.getBounds().getScale(mapView.getWidth(), mapView.getHeight());
898        if (minScale <= mapScale && mapScale < maxScale) {
899            mapView.zoomTo(v.getBounds().getCenter());
900        } else {
901            zoomTo(v.getBounds());
902        }
903    }
904
905    private static class ZoomData {
906        private final EastNorth center;
907        private final double scale;
908
909        ZoomData(EastNorth center, double scale) {
910            this.center = center;
911            this.scale = scale;
912        }
913
914        public EastNorth getCenterEastNorth() {
915            return center;
916        }
917
918        public double getScale() {
919            return scale;
920        }
921    }
922
923    private final transient Stack<ZoomData> zoomUndoBuffer = new Stack<>();
924    private final transient Stack<ZoomData> zoomRedoBuffer = new Stack<>();
925    private long zoomTimestamp = System.currentTimeMillis();
926
927    private void pushZoomUndo(EastNorth center, double scale) {
928        long now = System.currentTimeMillis();
929        if ((now - zoomTimestamp) > (Config.getPref().getDouble("zoom.undo.delay", 1.0) * 1000)) {
930            zoomUndoBuffer.push(new ZoomData(center, scale));
931            if (zoomUndoBuffer.size() > Config.getPref().getInt("zoom.undo.max", 50)) {
932                zoomUndoBuffer.remove(0);
933            }
934            zoomRedoBuffer.clear();
935        }
936        zoomTimestamp = now;
937    }
938
939    /**
940     * Zoom to previous location.
941     */
942    public void zoomPrevious() {
943        if (!zoomUndoBuffer.isEmpty()) {
944            ZoomData zoom = zoomUndoBuffer.pop();
945            zoomRedoBuffer.push(new ZoomData(getCenter(), getScale()));
946            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
947        }
948    }
949
950    /**
951     * Zoom to next location.
952     */
953    public void zoomNext() {
954        if (!zoomRedoBuffer.isEmpty()) {
955            ZoomData zoom = zoomRedoBuffer.pop();
956            zoomUndoBuffer.push(new ZoomData(getCenter(), getScale()));
957            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
958        }
959    }
960
961    /**
962     * Determines if zoom history contains "undo" entries.
963     * @return {@code true} if zoom history contains "undo" entries
964     */
965    public boolean hasZoomUndoEntries() {
966        return !zoomUndoBuffer.isEmpty();
967    }
968
969    /**
970     * Determines if zoom history contains "redo" entries.
971     * @return {@code true} if zoom history contains "redo" entries
972     */
973    public boolean hasZoomRedoEntries() {
974        return !zoomRedoBuffer.isEmpty();
975    }
976
977    private BBox getBBox(Point p, int snapDistance) {
978        return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
979                getLatLon(p.x + snapDistance, p.y + snapDistance));
980    }
981
982    /**
983     * The *result* does not depend on the current map selection state, neither does the result *order*.
984     * It solely depends on the distance to point p.
985     * @param p point
986     * @param predicate predicate to match
987     *
988     * @return a sorted map with the keys representing the distance of their associated nodes to point p.
989     */
990    private Map<Double, List<Node>> getNearestNodesImpl(Point p, Predicate<OsmPrimitive> predicate) {
991        Map<Double, List<Node>> nearestMap = new TreeMap<>();
992        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
993
994        if (ds != null) {
995            double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
996            snapDistanceSq *= snapDistanceSq;
997
998            for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
999                if (predicate.test(n)
1000                        && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
1001                    nearestMap.computeIfAbsent(dist, k -> new LinkedList<>()).add(n);
1002                }
1003            }
1004        }
1005
1006        return nearestMap;
1007    }
1008
1009    /**
1010     * The *result* does not depend on the current map selection state,
1011     * neither does the result *order*.
1012     * It solely depends on the distance to point p.
1013     *
1014     * @param p the point for which to search the nearest segment.
1015     * @param ignore a collection of nodes which are not to be returned.
1016     * @param predicate the returned objects have to fulfill certain properties.
1017     *
1018     * @return All nodes nearest to point p that are in a belt from
1019     *      dist(nearest) to dist(nearest)+4px around p and
1020     *      that are not in ignore.
1021     */
1022    public final List<Node> getNearestNodes(Point p,
1023            Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
1024        List<Node> nearestList = Collections.emptyList();
1025
1026        if (ignore == null) {
1027            ignore = Collections.emptySet();
1028        }
1029
1030        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1031        if (!nlists.isEmpty()) {
1032            Double minDistSq = null;
1033            for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1034                Double distSq = entry.getKey();
1035                List<Node> nlist = entry.getValue();
1036
1037                // filter nodes to be ignored before determining minDistSq..
1038                nlist.removeAll(ignore);
1039                if (minDistSq == null) {
1040                    if (!nlist.isEmpty()) {
1041                        minDistSq = distSq;
1042                        nearestList = new ArrayList<>();
1043                        nearestList.addAll(nlist);
1044                    }
1045                } else {
1046                    if (distSq-minDistSq < 16) {
1047                        nearestList.addAll(nlist);
1048                    }
1049                }
1050            }
1051        }
1052
1053        return nearestList;
1054    }
1055
1056    /**
1057     * The *result* does not depend on the current map selection state,
1058     * neither does the result *order*.
1059     * It solely depends on the distance to point p.
1060     *
1061     * @param p the point for which to search the nearest segment.
1062     * @param predicate the returned objects have to fulfill certain properties.
1063     *
1064     * @return All nodes nearest to point p that are in a belt from
1065     *      dist(nearest) to dist(nearest)+4px around p.
1066     * @see #getNearestNodes(Point, Collection, Predicate)
1067     */
1068    public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
1069        return getNearestNodes(p, null, predicate);
1070    }
1071
1072    /**
1073     * The *result* depends on the current map selection state IF use_selected is true.
1074     *
1075     * If more than one node within node.snap-distance pixels is found,
1076     * the nearest node selected is returned IF use_selected is true.
1077     *
1078     * Else the nearest new/id=0 node within about the same distance
1079     * as the true nearest node is returned.
1080     *
1081     * If no such node is found either, the true nearest node to p is returned.
1082     *
1083     * Finally, if a node is not found at all, null is returned.
1084     *
1085     * @param p the screen point
1086     * @param predicate this parameter imposes a condition on the returned object, e.g.
1087     *        give the nearest node that is tagged.
1088     * @param useSelected make search depend on selection
1089     *
1090     * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1091     */
1092    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1093        return getNearestNode(p, predicate, useSelected, null);
1094    }
1095
1096    /**
1097     * The *result* depends on the current map selection state IF use_selected is true
1098     *
1099     * If more than one node within node.snap-distance pixels is found,
1100     * the nearest node selected is returned IF use_selected is true.
1101     *
1102     * If there are no selected nodes near that point, the node that is related to some of the preferredRefs
1103     *
1104     * Else the nearest new/id=0 node within about the same distance
1105     * as the true nearest node is returned.
1106     *
1107     * If no such node is found either, the true nearest node to p is returned.
1108     *
1109     * Finally, if a node is not found at all, null is returned.
1110     *
1111     * @param p the screen point
1112     * @param predicate this parameter imposes a condition on the returned object, e.g.
1113     *        give the nearest node that is tagged.
1114     * @param useSelected make search depend on selection
1115     * @param preferredRefs primitives, whose nodes we prefer
1116     *
1117     * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1118     * @since 6065
1119     */
1120    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
1121            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1122
1123        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1124        if (nlists.isEmpty()) return null;
1125
1126        if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
1127        Node ntsel = null, ntnew = null, ntref = null;
1128        boolean useNtsel = useSelected;
1129        double minDistSq = nlists.keySet().iterator().next();
1130
1131        for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1132            Double distSq = entry.getKey();
1133            for (Node nd : entry.getValue()) {
1134                // find the nearest selected node
1135                if (ntsel == null && nd.isSelected()) {
1136                    ntsel = nd;
1137                    // if there are multiple nearest nodes, prefer the one
1138                    // that is selected. This is required in order to drag
1139                    // the selected node if multiple nodes have the same
1140                    // coordinates (e.g. after unglue)
1141                    useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
1142                }
1143                if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
1144                    List<OsmPrimitive> ndRefs = nd.getReferrers();
1145                    if (preferredRefs.stream().anyMatch(ndRefs::contains)) {
1146                        ntref = nd;
1147                    }
1148                }
1149                // find the nearest newest node that is within about the same
1150                // distance as the true nearest node
1151                if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
1152                    ntnew = nd;
1153                }
1154            }
1155        }
1156
1157        // take nearest selected, nearest new or true nearest node to p, in that order
1158        if (ntsel != null && useNtsel)
1159            return ntsel;
1160        if (ntref != null)
1161            return ntref;
1162        if (ntnew != null)
1163            return ntnew;
1164        return nlists.values().iterator().next().get(0);
1165    }
1166
1167    /**
1168     * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
1169     * @param p the screen point
1170     * @param predicate this parameter imposes a condition on the returned object, e.g.
1171     *        give the nearest node that is tagged.
1172     *
1173     * @return The nearest node to point p.
1174     */
1175    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
1176        return getNearestNode(p, predicate, true);
1177    }
1178
1179    /**
1180     * The *result* does not depend on the current map selection state, neither does the result *order*.
1181     * It solely depends on the distance to point p.
1182     * @param p the screen point
1183     * @param predicate this parameter imposes a condition on the returned object, e.g.
1184     *        give the nearest node that is tagged.
1185     *
1186     * @return a sorted map with the keys representing the perpendicular
1187     *      distance of their associated way segments to point p.
1188     */
1189    private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
1190        Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
1191        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1192
1193        if (ds != null) {
1194            double snapDistanceSq = Config.getPref().getInt("mappaint.segment.snap-distance", 10);
1195            snapDistanceSq *= snapDistanceSq;
1196
1197            for (Way w : ds.searchWays(getBBox(p, Config.getPref().getInt("mappaint.segment.snap-distance", 10)))) {
1198                if (!predicate.test(w)) {
1199                    continue;
1200                }
1201                Node lastN = null;
1202                int i = -2;
1203                for (Node n : w.getNodes()) {
1204                    i++;
1205                    if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
1206                        continue;
1207                    }
1208                    if (lastN == null) {
1209                        lastN = n;
1210                        continue;
1211                    }
1212
1213                    Point2D pA = getPoint2D(lastN);
1214                    Point2D pB = getPoint2D(n);
1215                    double c = pA.distanceSq(pB);
1216                    double a = p.distanceSq(pB);
1217                    double b = p.distanceSq(pA);
1218
1219                    /* perpendicular distance squared
1220                     * loose some precision to account for possible deviations in the calculation above
1221                     * e.g. if identical (A and B) come about reversed in another way, values may differ
1222                     * -- zero out least significant 32 dual digits of mantissa..
1223                     */
1224                    double perDistSq = Double.longBitsToDouble(
1225                            Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c)
1226                            >> 32 << 32); // resolution in numbers with large exponent not needed here..
1227
1228                    if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
1229                        nearestMap.computeIfAbsent(perDistSq, k -> new LinkedList<>()).add(new WaySegment(w, i));
1230                    }
1231
1232                    lastN = n;
1233                }
1234            }
1235        }
1236
1237        return nearestMap;
1238    }
1239
1240    /**
1241     * The result *order* depends on the current map selection state.
1242     * Segments within 10px of p are searched and sorted by their distance to {@code p},
1243     * then, within groups of equally distant segments, prefer those that are selected.
1244     *
1245     * @param p the point for which to search the nearest segments.
1246     * @param ignore a collection of segments which are not to be returned.
1247     * @param predicate the returned objects have to fulfill certain properties.
1248     *
1249     * @return all segments within 10px of p that are not in ignore,
1250     *          sorted by their perpendicular distance.
1251     */
1252    public final List<WaySegment> getNearestWaySegments(Point p,
1253            Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
1254        List<WaySegment> nearestList = new ArrayList<>();
1255        List<WaySegment> unselected = new LinkedList<>();
1256
1257        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1258            // put selected waysegs within each distance group first
1259            // makes the order of nearestList dependent on current selection state
1260            for (WaySegment ws : wss) {
1261                (ws.getWay().isSelected() ? nearestList : unselected).add(ws);
1262            }
1263            nearestList.addAll(unselected);
1264            unselected.clear();
1265        }
1266        if (ignore != null) {
1267            nearestList.removeAll(ignore);
1268        }
1269
1270        return nearestList;
1271    }
1272
1273    /**
1274     * The result *order* depends on the current map selection state.
1275     *
1276     * @param p the point for which to search the nearest segments.
1277     * @param predicate the returned objects have to fulfill certain properties.
1278     *
1279     * @return all segments within 10px of p, sorted by their perpendicular distance.
1280     * @see #getNearestWaySegments(Point, Collection, Predicate)
1281     */
1282    public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
1283        return getNearestWaySegments(p, null, predicate);
1284    }
1285
1286    /**
1287     * The *result* depends on the current map selection state IF use_selected is true.
1288     *
1289     * @param p the point for which to search the nearest segment.
1290     * @param predicate the returned object has to fulfill certain properties.
1291     * @param useSelected whether selected way segments should be preferred.
1292     *
1293     * @return The nearest way segment to point p,
1294     *      and, depending on use_selected, prefers a selected way segment, if found.
1295     * @see #getNearestWaySegments(Point, Collection, Predicate)
1296     */
1297    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1298        WaySegment wayseg = null;
1299        WaySegment ntsel = null;
1300
1301        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1302            if (wayseg != null && ntsel != null) {
1303                break;
1304            }
1305            for (WaySegment ws : wslist) {
1306                if (wayseg == null) {
1307                    wayseg = ws;
1308                }
1309                if (ntsel == null && ws.getWay().isSelected()) {
1310                    ntsel = ws;
1311                }
1312            }
1313        }
1314
1315        return (ntsel != null && useSelected) ? ntsel : wayseg;
1316    }
1317
1318    /**
1319     * The *result* depends on the current map selection state IF use_selected is true.
1320     *
1321     * @param p the point for which to search the nearest segment.
1322     * @param predicate the returned object has to fulfill certain properties.
1323     * @param useSelected whether selected way segments should be preferred.
1324     * @param preferredRefs - prefer segments related to these primitives, may be null
1325     *
1326     * @return The nearest way segment to point p,
1327     *      and, depending on use_selected, prefers a selected way segment, if found.
1328     * Also prefers segments of ways that are related to one of preferredRefs primitives
1329     *
1330     * @see #getNearestWaySegments(Point, Collection, Predicate)
1331     * @since 6065
1332     */
1333    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
1334            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1335        WaySegment wayseg = null;
1336        if (preferredRefs != null && preferredRefs.isEmpty())
1337            preferredRefs = null;
1338
1339        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1340            for (WaySegment ws : wslist) {
1341                if (wayseg == null) {
1342                    wayseg = ws;
1343                }
1344                if (useSelected && ws.getWay().isSelected()) {
1345                    return ws;
1346                }
1347                if (!Utils.isEmpty(preferredRefs)) {
1348                    // prefer ways containing given nodes
1349                    if (preferredRefs.contains(ws.getFirstNode()) || preferredRefs.contains(ws.getSecondNode())) {
1350                        return ws;
1351                    }
1352                    Collection<OsmPrimitive> wayRefs = ws.getWay().getReferrers();
1353                    // prefer member of the given relations
1354                    for (OsmPrimitive ref: preferredRefs) {
1355                        if (ref instanceof Relation && wayRefs.contains(ref)) {
1356                            return ws;
1357                        }
1358                    }
1359                }
1360            }
1361        }
1362        return wayseg;
1363    }
1364
1365    /**
1366     * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
1367     * @param p the point for which to search the nearest segment.
1368     * @param predicate the returned object has to fulfill certain properties.
1369     *
1370     * @return The nearest way segment to point p.
1371     */
1372    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
1373        return getNearestWaySegment(p, predicate, true);
1374    }
1375
1376    /**
1377     * The *result* does not depend on the current map selection state,
1378     * neither does the result *order*.
1379     * It solely depends on the perpendicular distance to point p.
1380     *
1381     * @param p the point for which to search the nearest ways.
1382     * @param ignore a collection of ways which are not to be returned.
1383     * @param predicate the returned object has to fulfill certain properties.
1384     *
1385     * @return all nearest ways to the screen point given that are not in ignore.
1386     * @see #getNearestWaySegments(Point, Collection, Predicate)
1387     */
1388    public final List<Way> getNearestWays(Point p,
1389            Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
1390        Set<Way> wset = new HashSet<>();
1391
1392        List<Way> nearestList = getNearestWaySegmentsImpl(p, predicate).values().stream()
1393                .flatMap(Collection::stream)
1394                .filter(ws -> wset.add(ws.getWay()))
1395                .map(ws -> ws.getWay())
1396                .collect(Collectors.toList());
1397        if (ignore != null) {
1398            nearestList.removeAll(ignore);
1399        }
1400
1401        return nearestList;
1402    }
1403
1404    /**
1405     * The *result* does not depend on the current map selection state,
1406     * neither does the result *order*.
1407     * It solely depends on the perpendicular distance to point p.
1408     *
1409     * @param p the point for which to search the nearest ways.
1410     * @param predicate the returned object has to fulfill certain properties.
1411     *
1412     * @return all nearest ways to the screen point given.
1413     * @see #getNearestWays(Point, Collection, Predicate)
1414     */
1415    public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1416        return getNearestWays(p, null, predicate);
1417    }
1418
1419    /**
1420     * The *result* depends on the current map selection state.
1421     *
1422     * @param p the point for which to search the nearest segment.
1423     * @param predicate the returned object has to fulfill certain properties.
1424     *
1425     * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
1426     * @see #getNearestWaySegment(Point, Predicate)
1427     */
1428    public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
1429        WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
1430        return (nearestWaySeg == null) ? null : nearestWaySeg.getWay();
1431    }
1432
1433    /**
1434     * The *result* does not depend on the current map selection state,
1435     * neither does the result *order*.
1436     * It solely depends on the distance to point p.
1437     *
1438     * First, nodes will be searched. If there are nodes within BBox found,
1439     * return a collection of those nodes only.
1440     *
1441     * If no nodes are found, search for nearest ways. If there are ways
1442     * within BBox found, return a collection of those ways only.
1443     *
1444     * If nothing is found, return an empty collection.
1445     *
1446     * @param p The point on screen.
1447     * @param ignore a collection of ways which are not to be returned.
1448     * @param predicate the returned object has to fulfill certain properties.
1449     *
1450     * @return Primitives nearest to the given screen point that are not in ignore.
1451     * @see #getNearestNodes(Point, Collection, Predicate)
1452     * @see #getNearestWays(Point, Collection, Predicate)
1453     */
1454    public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
1455            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1456        List<OsmPrimitive> nearestList = Collections.emptyList();
1457        OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
1458
1459        if (osm != null) {
1460            if (osm instanceof Node) {
1461                nearestList = new ArrayList<>(getNearestNodes(p, predicate));
1462            } else if (osm instanceof Way) {
1463                nearestList = new ArrayList<>(getNearestWays(p, predicate));
1464            }
1465            if (ignore != null) {
1466                nearestList.removeAll(ignore);
1467            }
1468        }
1469
1470        return nearestList;
1471    }
1472
1473    /**
1474     * The *result* does not depend on the current map selection state,
1475     * neither does the result *order*.
1476     * It solely depends on the distance to point p.
1477     *
1478     * @param p The point on screen.
1479     * @param predicate the returned object has to fulfill certain properties.
1480     * @return Primitives nearest to the given screen point.
1481     * @see #getNearestNodesOrWays(Point, Collection, Predicate)
1482     */
1483    public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1484        return getNearestNodesOrWays(p, null, predicate);
1485    }
1486
1487    /**
1488     * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
1489     * It decides, whether to yield the node to be tested or look for further (way) candidates.
1490     *
1491     * @param osm node to check
1492     * @param p point clicked
1493     * @param useSelected whether to prefer selected nodes
1494     * @return true, if the node fulfills the properties of the function body
1495     */
1496    private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
1497        if (osm != null) {
1498            if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
1499            if (osm.isTagged()) return true;
1500            if (useSelected && osm.isSelected()) return true;
1501        }
1502        return false;
1503    }
1504
1505    /**
1506     * The *result* depends on the current map selection state IF use_selected is true.
1507     *
1508     * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
1509     * the nearest, selected node.  If not found, try {@link #getNearestWaySegment(Point, Predicate)}
1510     * to find the nearest selected way.
1511     *
1512     * IF use_selected is false, or if no selected primitive was found, do the following.
1513     *
1514     * If the nearest node found is within 4px of p, simply take it.
1515     * Else, find the nearest way segment. Then, if p is closer to its
1516     * middle than to the node, take the way segment, else take the node.
1517     *
1518     * Finally, if no nearest primitive is found at all, return null.
1519     *
1520     * @param p The point on screen.
1521     * @param predicate the returned object has to fulfill certain properties.
1522     * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
1523     *
1524     * @return A primitive within snap-distance to point p,
1525     *      that is chosen by the algorithm described.
1526     * @see #getNearestNode(Point, Predicate)
1527     * @see #getNearestWay(Point, Predicate)
1528     */
1529    public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1530        Collection<OsmPrimitive> sel;
1531        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
1532        if (useSelected && ds != null) {
1533            sel = ds.getSelected();
1534        } else {
1535            sel = null;
1536        }
1537        OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
1538
1539        if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
1540        WaySegment ws;
1541        if (useSelected) {
1542            ws = getNearestWaySegment(p, predicate, useSelected, sel);
1543        } else {
1544            ws = getNearestWaySegment(p, predicate, useSelected);
1545        }
1546        if (ws == null) return osm;
1547
1548        if ((ws.getWay().isSelected() && useSelected) || osm == null) {
1549            // either (no _selected_ nearest node found, if desired) or no nearest node was found
1550            osm = ws.getWay();
1551        } else {
1552            int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1553            maxWaySegLenSq *= maxWaySegLenSq;
1554
1555            Point2D wp1 = getPoint2D(ws.getFirstNode());
1556            Point2D wp2 = getPoint2D(ws.getSecondNode());
1557
1558            // is wayseg shorter than maxWaySegLenSq and
1559            // is p closer to the middle of wayseg  than  to the nearest node?
1560            if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1561                    p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
1562                osm = ws.getWay();
1563            }
1564        }
1565        return osm;
1566    }
1567
1568    /**
1569     * if r = 0 returns a, if r=1 returns b,
1570     * if r = 0.5 returns center between a and b, etc..
1571     *
1572     * @param r scale value
1573     * @param a root of vector
1574     * @param b vector
1575     * @return new point at a + r*(ab)
1576     */
1577    public static Point2D project(double r, Point2D a, Point2D b) {
1578        Point2D ret = null;
1579
1580        if (a != null && b != null) {
1581            ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1582                    a.getY() + r*(b.getY()-a.getY()));
1583        }
1584        return ret;
1585    }
1586
1587    /**
1588     * The *result* does not depend on the current map selection state, neither does the result *order*.
1589     * It solely depends on the distance to point p.
1590     *
1591     * @param p The point on screen.
1592     * @param ignore a collection of ways which are not to be returned.
1593     * @param predicate the returned object has to fulfill certain properties.
1594     *
1595     * @return a list of all objects that are nearest to point p and
1596     *          not in ignore or an empty list if nothing was found.
1597     */
1598    public final List<OsmPrimitive> getAllNearest(Point p,
1599            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1600        Set<Way> wset = new HashSet<>();
1601
1602        // add nearby ways
1603        List<OsmPrimitive> nearestList = getNearestWaySegmentsImpl(p, predicate).values().stream()
1604                .flatMap(Collection::stream)
1605                .filter(ws -> wset.add(ws.getWay()))
1606                .map(ws -> ws.getWay())
1607                .collect(Collectors.toList());
1608
1609        // add nearby nodes
1610        getNearestNodesImpl(p, predicate).values()
1611                .forEach(nearestList::addAll);
1612
1613        // add parent relations of nearby nodes and ways
1614        Set<OsmPrimitive> parentRelations = nearestList.stream()
1615                .flatMap(o -> o.referrers(Relation.class))
1616                .filter(predicate)
1617                .collect(Collectors.toSet());
1618        nearestList.addAll(parentRelations);
1619
1620        if (ignore != null) {
1621            nearestList.removeAll(ignore);
1622        }
1623
1624        return nearestList;
1625    }
1626
1627    /**
1628     * The *result* does not depend on the current map selection state, neither does the result *order*.
1629     * It solely depends on the distance to point p.
1630     *
1631     * @param p The point on screen.
1632     * @param predicate the returned object has to fulfill certain properties.
1633     *
1634     * @return a list of all objects that are nearest to point p
1635     *          or an empty list if nothing was found.
1636     * @see #getAllNearest(Point, Collection, Predicate)
1637     */
1638    public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1639        return getAllNearest(p, null, predicate);
1640    }
1641
1642    /**
1643     * Returns the projection to be used in calculating stuff.
1644     * @return The projection to be used in calculating stuff.
1645     */
1646    public Projection getProjection() {
1647        return state.getProjection();
1648    }
1649
1650    @Override
1651    public String helpTopic() {
1652        String n = getClass().getName();
1653        return n.substring(n.lastIndexOf('.')+1);
1654    }
1655
1656    /**
1657     * Return a ID which is unique as long as viewport dimensions are the same
1658     * @return A unique ID, as long as viewport dimensions are the same
1659     */
1660    public int getViewID() {
1661        EastNorth center = getCenter();
1662        String x = new StringBuilder().append(center.east())
1663                          .append('_').append(center.north())
1664                          .append('_').append(getScale())
1665                          .append('_').append(getWidth())
1666                          .append('_').append(getHeight())
1667                          .append('_').append(getProjection()).toString();
1668        CRC32 id = new CRC32();
1669        id.update(x.getBytes(StandardCharsets.UTF_8));
1670        return (int) id.getValue();
1671    }
1672
1673    /**
1674     * Set new cursor.
1675     * @param cursor The new cursor to use.
1676     * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1677     */
1678    public void setNewCursor(Cursor cursor, Object reference) {
1679        cursorManager.setNewCursor(cursor, reference);
1680    }
1681
1682    /**
1683     * Set new cursor.
1684     * @param cursor the type of predefined cursor
1685     * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1686     */
1687    public void setNewCursor(int cursor, Object reference) {
1688        setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1689    }
1690
1691    /**
1692     * Remove the new cursor and reset to previous
1693     * @param reference Cursor reference
1694     */
1695    public void resetCursor(Object reference) {
1696        cursorManager.resetCursor(reference);
1697    }
1698
1699    /**
1700     * Gets the cursor manager that is used for this NavigatableComponent.
1701     * @return The cursor manager.
1702     */
1703    public CursorManager getCursorManager() {
1704        return cursorManager;
1705    }
1706
1707    /**
1708     * Get a max scale for projection that describes world in 1/512 of the projection unit
1709     * @return max scale
1710     */
1711    public double getMaxScale() {
1712        ProjectionBounds world = getMaxProjectionBounds();
1713        return Math.max(
1714            world.maxNorth-world.minNorth,
1715            world.maxEast-world.minEast
1716        )/512;
1717    }
1718}