source: josm/trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java @ 12725

Last change on this file since 12725 was 12725, checked in by bastiK, 3 months ago

see #15229 - deprecate ILatLon#getEastNorth() so ILatLon has no dependency on Main.proj

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