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