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

Last change on this file since 12841 was 12841, checked in by bastiK, 5 weeks ago

see #15229 - fix deprecations caused by [12840]

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