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