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