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