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 dm = ll1.greatCircleDistance(ll2);
556            double den = 100 * scale;
557            double scaleMin = 0.01 * den / dm / 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(1000L / 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 pA = getPoint2D(lastN);
1029                    Point2D pB = getPoint2D(n);
1030                    double c = pA.distanceSq(pB);
1031                    double a = p.distanceSq(pB);
1032                    double b = p.distanceSq(pA);
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;
1121        WaySegment ntsel = null;
1122
1123        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1124            if (wayseg != null && ntsel != null) {
1125                break;
1126            }
1127            for (WaySegment ws : wslist) {
1128                if (wayseg == null) {
1129                    wayseg = ws;
1130                }
1131                if (ntsel == null && ws.way.isSelected()) {
1132                    ntsel = ws;
1133                }
1134            }
1135        }
1136
1137        return (ntsel != null && useSelected) ? ntsel : wayseg;
1138    }
1139
1140    /**
1141     * The *result* depends on the current map selection state IF use_selected is true.
1142     *
1143     * @param p the point for which to search the nearest segment.
1144     * @param predicate the returned object has to fulfill certain properties.
1145     * @param useSelected whether selected way segments should be preferred.
1146     * @param preferredRefs - prefer segments related to these primitives, may be null
1147     *
1148     * @return The nearest way segment to point p,
1149     *      and, depending on use_selected, prefers a selected way segment, if found.
1150     * Also prefers segments of ways that are related to one of preferredRefs primitives
1151     *
1152     * @see #getNearestWaySegments(Point, Collection, Predicate)
1153     * @since 6065
1154     */
1155    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
1156            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1157        WaySegment wayseg = null;
1158        WaySegment ntsel = null;
1159        WaySegment ntref = null;
1160        if (preferredRefs != null && preferredRefs.isEmpty())
1161            preferredRefs = null;
1162
1163        searchLoop: for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1164            for (WaySegment ws : wslist) {
1165                if (wayseg == null) {
1166                    wayseg = ws;
1167                }
1168                if (ntsel == null && ws.way.isSelected()) {
1169                    ntsel = ws;
1170                    break searchLoop;
1171                }
1172                if (ntref == null && preferredRefs != null) {
1173                    // prefer ways containing given nodes
1174                    for (Node nd: ws.way.getNodes()) {
1175                        if (preferredRefs.contains(nd)) {
1176                            ntref = ws;
1177                            break searchLoop;
1178                        }
1179                    }
1180                    Collection<OsmPrimitive> wayRefs = ws.way.getReferrers();
1181                    // prefer member of the given relations
1182                    for (OsmPrimitive ref: preferredRefs) {
1183                        if (ref instanceof Relation && wayRefs.contains(ref)) {
1184                            ntref = ws;
1185                            break searchLoop;
1186                        }
1187                    }
1188                }
1189            }
1190        }
1191        if (ntsel != null && useSelected)
1192            return ntsel;
1193        if (ntref != null)
1194            return ntref;
1195        return wayseg;
1196    }
1197
1198    /**
1199     * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
1200     * @param p the point for which to search the nearest segment.
1201     * @param predicate the returned object has to fulfill certain properties.
1202     *
1203     * @return The nearest way segment to point p.
1204     */
1205    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
1206        return getNearestWaySegment(p, predicate, true);
1207    }
1208
1209    /**
1210     * The *result* does not depend on the current map selection state,
1211     * neither does the result *order*.
1212     * It solely depends on the perpendicular distance to point p.
1213     *
1214     * @param p the point for which to search the nearest ways.
1215     * @param ignore a collection of ways which are not to be returned.
1216     * @param predicate the returned object has to fulfill certain properties.
1217     *
1218     * @return all nearest ways to the screen point given that are not in ignore.
1219     * @see #getNearestWaySegments(Point, Collection, Predicate)
1220     */
1221    public final List<Way> getNearestWays(Point p,
1222            Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
1223        List<Way> nearestList = new ArrayList<>();
1224        Set<Way> wset = new HashSet<>();
1225
1226        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1227            for (WaySegment ws : wss) {
1228                if (wset.add(ws.way)) {
1229                    nearestList.add(ws.way);
1230                }
1231            }
1232        }
1233        if (ignore != null) {
1234            nearestList.removeAll(ignore);
1235        }
1236
1237        return nearestList;
1238    }
1239
1240    /**
1241     * The *result* does not depend on the current map selection state,
1242     * neither does the result *order*.
1243     * It solely depends on the perpendicular distance to point p.
1244     *
1245     * @param p the point for which to search the nearest ways.
1246     * @param predicate the returned object has to fulfill certain properties.
1247     *
1248     * @return all nearest ways to the screen point given.
1249     * @see #getNearestWays(Point, Collection, Predicate)
1250     */
1251    public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1252        return getNearestWays(p, null, predicate);
1253    }
1254
1255    /**
1256     * The *result* depends on the current map selection state.
1257     *
1258     * @param p the point for which to search the nearest segment.
1259     * @param predicate the returned object has to fulfill certain properties.
1260     *
1261     * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
1262     * @see #getNearestWaySegment(Point, Predicate)
1263     */
1264    public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
1265        WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
1266        return (nearestWaySeg == null) ? null : nearestWaySeg.way;
1267    }
1268
1269    /**
1270     * The *result* does not depend on the current map selection state,
1271     * neither does the result *order*.
1272     * It solely depends on the distance to point p.
1273     *
1274     * First, nodes will be searched. If there are nodes within BBox found,
1275     * return a collection of those nodes only.
1276     *
1277     * If no nodes are found, search for nearest ways. If there are ways
1278     * within BBox found, return a collection of those ways only.
1279     *
1280     * If nothing is found, return an empty collection.
1281     *
1282     * @param p The point on screen.
1283     * @param ignore a collection of ways which are not to be returned.
1284     * @param predicate the returned object has to fulfill certain properties.
1285     *
1286     * @return Primitives nearest to the given screen point that are not in ignore.
1287     * @see #getNearestNodes(Point, Collection, Predicate)
1288     * @see #getNearestWays(Point, Collection, Predicate)
1289     */
1290    public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
1291            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1292        List<OsmPrimitive> nearestList = Collections.emptyList();
1293        OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
1294
1295        if (osm != null) {
1296            if (osm instanceof Node) {
1297                nearestList = new ArrayList<OsmPrimitive>(getNearestNodes(p, predicate));
1298            } else if (osm instanceof Way) {
1299                nearestList = new ArrayList<OsmPrimitive>(getNearestWays(p, predicate));
1300            }
1301            if (ignore != null) {
1302                nearestList.removeAll(ignore);
1303            }
1304        }
1305
1306        return nearestList;
1307    }
1308
1309    /**
1310     * The *result* does not depend on the current map selection state,
1311     * neither does the result *order*.
1312     * It solely depends on the distance to point p.
1313     *
1314     * @param p The point on screen.
1315     * @param predicate the returned object has to fulfill certain properties.
1316     * @return Primitives nearest to the given screen point.
1317     * @see #getNearestNodesOrWays(Point, Collection, Predicate)
1318     */
1319    public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1320        return getNearestNodesOrWays(p, null, predicate);
1321    }
1322
1323    /**
1324     * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
1325     * It decides, whether to yield the node to be tested or look for further (way) candidates.
1326     *
1327     * @param osm node to check
1328     * @param p point clicked
1329     * @param useSelected whether to prefer selected nodes
1330     * @return true, if the node fulfills the properties of the function body
1331     */
1332    private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
1333        if (osm != null) {
1334            if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
1335            if (osm.isTagged()) return true;
1336            if (useSelected && osm.isSelected()) return true;
1337        }
1338        return false;
1339    }
1340
1341    /**
1342     * The *result* depends on the current map selection state IF use_selected is true.
1343     *
1344     * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
1345     * the nearest, selected node.  If not found, try {@link #getNearestWaySegment(Point, Predicate)}
1346     * to find the nearest selected way.
1347     *
1348     * IF use_selected is false, or if no selected primitive was found, do the following.
1349     *
1350     * If the nearest node found is within 4px of p, simply take it.
1351     * Else, find the nearest way segment. Then, if p is closer to its
1352     * middle than to the node, take the way segment, else take the node.
1353     *
1354     * Finally, if no nearest primitive is found at all, return null.
1355     *
1356     * @param p The point on screen.
1357     * @param predicate the returned object has to fulfill certain properties.
1358     * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
1359     *
1360     * @return A primitive within snap-distance to point p,
1361     *      that is chosen by the algorithm described.
1362     * @see #getNearestNode(Point, Predicate)
1363     * @see #getNearestWay(Point, Predicate)
1364     */
1365    public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1366        Collection<OsmPrimitive> sel;
1367        DataSet ds = getCurrentDataSet();
1368        if (useSelected && ds != null) {
1369            sel = ds.getSelected();
1370        } else {
1371            sel = null;
1372        }
1373        OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
1374
1375        if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
1376        WaySegment ws;
1377        if (useSelected) {
1378            ws = getNearestWaySegment(p, predicate, useSelected, sel);
1379        } else {
1380            ws = getNearestWaySegment(p, predicate, useSelected);
1381        }
1382        if (ws == null) return osm;
1383
1384        if ((ws.way.isSelected() && useSelected) || osm == null) {
1385            // either (no _selected_ nearest node found, if desired) or no nearest node was found
1386            osm = ws.way;
1387        } else {
1388            int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1389            maxWaySegLenSq *= maxWaySegLenSq;
1390
1391            Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
1392            Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
1393
1394            // is wayseg shorter than maxWaySegLenSq and
1395            // is p closer to the middle of wayseg  than  to the nearest node?
1396            if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1397                    p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
1398                osm = ws.way;
1399            }
1400        }
1401        return osm;
1402    }
1403
1404    public static double perDist(Point2D pt, Point2D a, Point2D b) {
1405        if (pt != null && a != null && b != null) {
1406            double pd =
1407                    (a.getX()-pt.getX())*(b.getX()-a.getX()) -
1408                    (a.getY()-pt.getY())*(b.getY()-a.getY());
1409            return Math.abs(pd) / a.distance(b);
1410        }
1411        return 0d;
1412    }
1413
1414    /**
1415     *
1416     * @param pt point to project onto (ab)
1417     * @param a root of vector
1418     * @param b vector
1419     * @return point of intersection of line given by (ab)
1420     *      with its orthogonal line running through pt
1421     */
1422    public static Point2D project(Point2D pt, Point2D a, Point2D b) {
1423        if (pt != null && a != null && b != null) {
1424            double r = (
1425                    (pt.getX()-a.getX())*(b.getX()-a.getX()) +
1426                    (pt.getY()-a.getY())*(b.getY()-a.getY()))
1427                    / a.distanceSq(b);
1428            return project(r, a, b);
1429        }
1430        return null;
1431    }
1432
1433    /**
1434     * if r = 0 returns a, if r=1 returns b,
1435     * if r = 0.5 returns center between a and b, etc..
1436     *
1437     * @param r scale value
1438     * @param a root of vector
1439     * @param b vector
1440     * @return new point at a + r*(ab)
1441     */
1442    public static Point2D project(double r, Point2D a, Point2D b) {
1443        Point2D ret = null;
1444
1445        if (a != null && b != null) {
1446            ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1447                    a.getY() + r*(b.getY()-a.getY()));
1448        }
1449        return ret;
1450    }
1451
1452    /**
1453     * The *result* does not depend on the current map selection state, neither does the result *order*.
1454     * It solely depends on the distance to point p.
1455     *
1456     * @param p The point on screen.
1457     * @param ignore a collection of ways which are not to be returned.
1458     * @param predicate the returned object has to fulfill certain properties.
1459     *
1460     * @return a list of all objects that are nearest to point p and
1461     *          not in ignore or an empty list if nothing was found.
1462     */
1463    public final List<OsmPrimitive> getAllNearest(Point p,
1464            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1465        List<OsmPrimitive> nearestList = new ArrayList<>();
1466        Set<Way> wset = new HashSet<>();
1467
1468        // add nearby ways
1469        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1470            for (WaySegment ws : wss) {
1471                if (wset.add(ws.way)) {
1472                    nearestList.add(ws.way);
1473                }
1474            }
1475        }
1476
1477        // add nearby nodes
1478        for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
1479            nearestList.addAll(nlist);
1480        }
1481
1482        // add parent relations of nearby nodes and ways
1483        Set<OsmPrimitive> parentRelations = new HashSet<>();
1484        for (OsmPrimitive o : nearestList) {
1485            for (OsmPrimitive r : o.getReferrers()) {
1486                if (r instanceof Relation && predicate.evaluate(r)) {
1487                    parentRelations.add(r);
1488                }
1489            }
1490        }
1491        nearestList.addAll(parentRelations);
1492
1493        if (ignore != null) {
1494            nearestList.removeAll(ignore);
1495        }
1496
1497        return nearestList;
1498    }
1499
1500    /**
1501     * The *result* does not depend on the current map selection state, neither does the result *order*.
1502     * It solely depends on the distance to point p.
1503     *
1504     * @param p The point on screen.
1505     * @param predicate the returned object has to fulfill certain properties.
1506     *
1507     * @return a list of all objects that are nearest to point p
1508     *          or an empty list if nothing was found.
1509     * @see #getAllNearest(Point, Collection, Predicate)
1510     */
1511    public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1512        return getAllNearest(p, null, predicate);
1513    }
1514
1515    /**
1516     * @return The projection to be used in calculating stuff.
1517     */
1518    public Projection getProjection() {
1519        return Main.getProjection();
1520    }
1521
1522    @Override
1523    public String helpTopic() {
1524        String n = getClass().getName();
1525        return n.substring(n.lastIndexOf('.')+1);
1526    }
1527
1528    /**
1529     * Return a ID which is unique as long as viewport dimensions are the same
1530     * @return A unique ID, as long as viewport dimensions are the same
1531     */
1532    public int getViewID() {
1533        String x = center.east() + '_' + center.north() + '_' + scale + '_' +
1534                getWidth() + '_' + getHeight() + '_' + getProjection().toString();
1535        CRC32 id = new CRC32();
1536        id.update(x.getBytes(StandardCharsets.UTF_8));
1537        return (int) id.getValue();
1538    }
1539
1540    /**
1541     * Set new cursor.
1542     * @param cursor The new cursor to use.
1543     * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1544     */
1545    public void setNewCursor(Cursor cursor, Object reference) {
1546        cursorManager.setNewCursor(cursor, reference);
1547    }
1548
1549    /**
1550     * Set new cursor.
1551     * @param cursor the type of predefined cursor
1552     * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1553     */
1554    public void setNewCursor(int cursor, Object reference) {
1555        setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1556    }
1557
1558    /**
1559     * Remove the new cursor and reset to previous
1560     * @param reference Cursor reference
1561     */
1562    public void resetCursor(Object reference) {
1563        cursorManager.resetCursor(reference);
1564    }
1565
1566    /**
1567     * Gets the cursor manager that is used for this NavigatableComponent.
1568     * @return The cursor manager.
1569     */
1570    public CursorManager getCursorManager() {
1571        return cursorManager;
1572    }
1573
1574    @Override
1575    public void paint(Graphics g) {
1576        synchronized (paintRequestLock) {
1577            if (paintRect != null) {
1578                Graphics g2 = g.create();
1579                g2.setColor(Utils.complement(PaintColors.getBackgroundColor()));
1580                g2.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
1581                g2.dispose();
1582            }
1583            if (paintPoly != null) {
1584                Graphics g2 = g.create();
1585                g2.setColor(Utils.complement(PaintColors.getBackgroundColor()));
1586                g2.drawPolyline(paintPoly.xpoints, paintPoly.ypoints, paintPoly.npoints);
1587                g2.dispose();
1588            }
1589        }
1590        super.paint(g);
1591    }
1592
1593    /**
1594     * Requests to paint the given {@code Rectangle}.
1595     * @param r The Rectangle to draw
1596     * @see #requestClearRect
1597     * @since 5500
1598     */
1599    public void requestPaintRect(Rectangle r) {
1600        if (r != null) {
1601            synchronized (paintRequestLock) {
1602                paintRect = r;
1603            }
1604            repaint();
1605        }
1606    }
1607
1608    /**
1609     * Requests to paint the given {@code Polygon} as a polyline (unclosed polygon).
1610     * @param p The Polygon to draw
1611     * @see #requestClearPoly
1612     * @since 5500
1613     */
1614    public void requestPaintPoly(Polygon p) {
1615        if (p != null) {
1616            synchronized (paintRequestLock) {
1617                paintPoly = p;
1618            }
1619            repaint();
1620        }
1621    }
1622
1623    /**
1624     * Requests to clear the rectangled previously drawn.
1625     * @see #requestPaintRect
1626     * @since 5500
1627     */
1628    public void requestClearRect() {
1629        synchronized (paintRequestLock) {
1630            paintRect = null;
1631        }
1632        repaint();
1633    }
1634
1635    /**
1636     * Requests to clear the polyline previously drawn.
1637     * @see #requestPaintPoly
1638     * @since 5500
1639     */
1640    public void requestClearPoly() {
1641        synchronized (paintRequestLock) {
1642            paintPoly = null;
1643        }
1644        repaint();
1645    }
1646
1647    /**
1648     * Get a max scale for projection that describes world in 1/512 of the projection unit
1649     * @return max scale
1650     */
1651    public double getMaxScale() {
1652        ProjectionBounds world = getMaxProjectionBounds();
1653        return Math.max(
1654            world.maxNorth-world.minNorth,
1655            world.maxEast-world.minEast
1656        )/512;
1657    }
1658}