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