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