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

Last change on this file since 11535 was 11535, checked in by Don-vip, 2 years ago

sonar - squid:S2142 - "InterruptedException" should not be ignored

  • Property svn:eol-style set to native
File size: 60.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.CachedLatLon;
40import org.openstreetmap.josm.data.coor.EastNorth;
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.Projections;
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.Utils;
63
64/**
65 * A component that can be navigated by a {@link MapMover}. Used as map view and for the
66 * zoomer in the download dialog.
67 *
68 * @author imi
69 * @since 41
70 */
71public class NavigatableComponent extends JComponent implements Helpful {
72
73    /**
74     * Interface to notify listeners of the change of the zoom area.
75     * @since 10600 (functional interface)
76     */
77    @FunctionalInterface
78    public interface ZoomChangeListener {
79        /**
80         * Method called when the zoom area has changed.
81         */
82        void zoomChanged();
83    }
84
85    /**
86     * To determine if a primitive is currently selectable.
87     */
88    public transient Predicate<OsmPrimitive> isSelectablePredicate = prim -> {
89        if (!prim.isSelectable()) return false;
90        // if it isn't displayed on screen, you cannot click on it
91        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
92        try {
93            return !MapPaintStyles.getStyles().get(prim, getDist100Pixel(), this).isEmpty();
94        } finally {
95            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
96        }
97    };
98
99    /** Snap distance */
100    public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
101    /** Zoom steps to get double scale */
102    public static final DoubleProperty PROP_ZOOM_RATIO = new DoubleProperty("zoom.ratio", 2.0);
103    /** Divide intervals between native resolution levels to smaller steps if they are much larger than zoom ratio */
104    public static final BooleanProperty PROP_ZOOM_INTERMEDIATE_STEPS = new BooleanProperty("zoom.intermediate-steps", true);
105
106    /** Property name for center change events */
107    public static final String PROPNAME_CENTER = "center";
108    /** Property name for scale change events */
109    public static final String PROPNAME_SCALE = "scale";
110
111    /**
112     * The layer which scale is set to.
113     */
114    private transient NativeScaleLayer nativeScaleLayer;
115
116    /**
117     * the zoom listeners
118     */
119    private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();
120
121    /**
122     * Removes a zoom change listener
123     *
124     * @param listener the listener. Ignored if null or already absent
125     */
126    public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
127        zoomChangeListeners.remove(listener);
128    }
129
130    /**
131     * Adds a zoom change listener
132     *
133     * @param listener the listener. Ignored if null or already registered.
134     */
135    public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
136        if (listener != null) {
137            zoomChangeListeners.addIfAbsent(listener);
138        }
139    }
140
141    protected static void fireZoomChanged() {
142        for (ZoomChangeListener l : zoomChangeListeners) {
143            l.zoomChanged();
144        }
145    }
146
147    // The only events that may move/resize this map view are window movements or changes to the map view size.
148    // We can clean this up more by only recalculating the state on repaint.
149    private final transient HierarchyListener hierarchyListener = e -> {
150        long interestingFlags = HierarchyEvent.ANCESTOR_MOVED | HierarchyEvent.SHOWING_CHANGED;
151        if ((e.getChangeFlags() & interestingFlags) != 0) {
152            updateLocationState();
153        }
154    };
155
156    private final transient ComponentAdapter componentListener = new ComponentAdapter() {
157        @Override
158        public void componentShown(ComponentEvent e) {
159            updateLocationState();
160        }
161
162        @Override
163        public void componentResized(ComponentEvent e) {
164            updateLocationState();
165        }
166    };
167
168    protected transient ViewportData initialViewport;
169
170    protected final transient CursorManager cursorManager = new CursorManager(this);
171
172    /**
173     * The current state (scale, center, ...) of this map view.
174     */
175    private transient MapViewState state;
176
177    /**
178     * Constructs a new {@code NavigatableComponent}.
179     */
180    public NavigatableComponent() {
181        setLayout(null);
182        state = MapViewState.createDefaultState(getWidth(), getHeight());
183        // uses weak link.
184        Main.addProjectionChangeListener((oldValue, newValue) -> fixProjection());
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.getCenterAtPixel().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.getCenterAtPixel().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.getCenterAtPixel().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     * @param latlon The point, where this geopoint would be drawn.
513     * @return The point on screen where "point" would be drawn, relative to the own top/left.
514     */
515    public Point2D getPoint2D(LatLon latlon) {
516        if (latlon == null)
517            return new Point();
518        else if (latlon instanceof CachedLatLon)
519            return getPoint2D(((CachedLatLon) latlon).getEastNorth());
520        else
521            return getPoint2D(getProjection().latlon2eastNorth(latlon));
522    }
523
524    /**
525     * Return the point on the screen where this Node would be.
526     * @param n The node, where this geopoint would be drawn.
527     * @return The point on screen where "node" would be drawn, relative to the own top/left.
528     */
529    public Point2D getPoint2D(Node n) {
530        return getPoint2D(n.getEastNorth());
531    }
532
533    /**
534     * looses precision, may overflow (depends on p and current scale)
535     * @param p east/north
536     * @return point
537     * @see #getPoint2D(EastNorth)
538     */
539    public Point getPoint(EastNorth p) {
540        Point2D d = getPoint2D(p);
541        return new Point((int) d.getX(), (int) d.getY());
542    }
543
544    /**
545     * looses precision, may overflow (depends on p and current scale)
546     * @param latlon lat/lon
547     * @return point
548     * @see #getPoint2D(LatLon)
549     */
550    public Point getPoint(LatLon latlon) {
551        Point2D d = getPoint2D(latlon);
552        return new Point((int) d.getX(), (int) d.getY());
553    }
554
555    /**
556     * looses precision, may overflow (depends on p and current scale)
557     * @param n node
558     * @return point
559     * @see #getPoint2D(Node)
560     */
561    public Point getPoint(Node n) {
562        Point2D d = getPoint2D(n);
563        return new Point((int) d.getX(), (int) d.getY());
564    }
565
566    /**
567     * Zoom to the given coordinate and scale.
568     *
569     * @param newCenter The center x-value (easting) to zoom to.
570     * @param newScale The scale to use.
571     */
572    public void zoomTo(EastNorth newCenter, double newScale) {
573        zoomTo(newCenter, newScale, false);
574    }
575
576    /**
577     * Zoom to the given coordinate and scale.
578     *
579     * @param center The center x-value (easting) to zoom to.
580     * @param scale The scale to use.
581     * @param initial true if this call initializes the viewport.
582     */
583    public void zoomTo(EastNorth center, double scale, boolean initial) {
584        Bounds b = getProjection().getWorldBoundsLatLon();
585        ProjectionBounds pb = getProjection().getWorldBoundsBoxEastNorth();
586        double newScale = scale;
587        int width = getWidth();
588        int height = getHeight();
589
590        // make sure, the center of the screen is within projection bounds
591        double east = center.east();
592        double north = center.north();
593        east = Math.max(east, pb.minEast);
594        east = Math.min(east, pb.maxEast);
595        north = Math.max(north, pb.minNorth);
596        north = Math.min(north, pb.maxNorth);
597        EastNorth newCenter = new EastNorth(east, north);
598
599        // don't zoom out too much, the world bounds should be at least
600        // half the size of the screen
601        double pbHeight = pb.maxNorth - pb.minNorth;
602        if (height > 0 && 2 * pbHeight < height * newScale) {
603            double newScaleH = 2 * pbHeight / height;
604            double pbWidth = pb.maxEast - pb.minEast;
605            if (width > 0 && 2 * pbWidth < width * newScale) {
606                double newScaleW = 2 * pbWidth / width;
607                newScale = Math.max(newScaleH, newScaleW);
608            }
609        }
610
611        // don't zoom in too much, minimum: 100 px = 1 cm
612        LatLon ll1 = getLatLon(width / 2 - 50, height / 2);
613        LatLon ll2 = getLatLon(width / 2 + 50, height / 2);
614        if (ll1.isValid() && ll2.isValid() && b.contains(ll1) && b.contains(ll2)) {
615            double dm = ll1.greatCircleDistance(ll2);
616            double den = 100 * getScale();
617            double scaleMin = 0.01 * den / dm / 100;
618            if (newScale < scaleMin && !Double.isInfinite(scaleMin)) {
619                newScale = scaleMin;
620            }
621        }
622
623        // snap scale to imagery if needed
624        newScale = scaleRound(newScale);
625
626        if (!newCenter.equals(getCenter()) || !Utils.equalsEpsilon(getScale(), newScale)) {
627            if (!initial) {
628                pushZoomUndo(getCenter(), getScale());
629            }
630            zoomNoUndoTo(newCenter, newScale, initial);
631        }
632    }
633
634    /**
635     * Zoom to the given coordinate without adding to the zoom undo buffer.
636     *
637     * @param newCenter The center x-value (easting) to zoom to.
638     * @param newScale The scale to use.
639     * @param initial true if this call initializes the viewport.
640     */
641    private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) {
642        if (!newCenter.equals(getCenter())) {
643            EastNorth oldCenter = getCenter();
644            state = state.movedTo(state.getCenterAtPixel(), newCenter);
645            if (!initial) {
646                firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter);
647            }
648        }
649        if (!Utils.equalsEpsilon(getScale(), newScale)) {
650            double oldScale = getScale();
651            state = state.usingScale(newScale);
652            // temporary. Zoom logic needs to be moved.
653            state = state.movedTo(state.getCenterAtPixel(), newCenter);
654            if (!initial) {
655                firePropertyChange(PROPNAME_SCALE, oldScale, newScale);
656            }
657        }
658
659        if (!initial) {
660            repaint();
661            fireZoomChanged();
662        }
663    }
664
665    /**
666     * Zoom to given east/north.
667     * @param newCenter new center coordinates
668     */
669    public void zoomTo(EastNorth newCenter) {
670        zoomTo(newCenter, getScale());
671    }
672
673    /**
674     * Zoom to given lat/lon.
675     * @param newCenter new center coordinates
676     */
677    public void zoomTo(LatLon newCenter) {
678        zoomTo(Projections.project(newCenter));
679    }
680
681    /**
682     * Create a thread that moves the viewport to the given center in an animated fashion.
683     * @param newCenter new east/north center
684     */
685    public void smoothScrollTo(EastNorth newCenter) {
686        // FIXME make these configurable.
687        final int fps = 20;     // animation frames per second
688        final int speed = 1500; // milliseconds for full-screen-width pan
689        if (!newCenter.equals(getCenter())) {
690            final EastNorth oldCenter = getCenter();
691            final double distance = newCenter.distance(oldCenter) / getScale();
692            final double milliseconds = distance / getWidth() * speed;
693            final double frames = milliseconds * fps / 1000;
694            final EastNorth finalNewCenter = newCenter;
695
696            new Thread("smooth-scroller") {
697                @Override
698                public void run() {
699                    for (int i = 0; i < frames; i++) {
700                        // FIXME - not use zoom history here
701                        zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames));
702                        try {
703                            Thread.sleep(1000L / fps);
704                        } catch (InterruptedException ex) {
705                            Main.warn("InterruptedException in "+NavigatableComponent.class.getSimpleName()+" during smooth scrolling");
706                            Thread.currentThread().interrupt();
707                        }
708                    }
709                }
710            }.start();
711        }
712    }
713
714    public void zoomManyTimes(double x, double y, int times) {
715        double oldScale = getScale();
716        double newScale = scaleZoomManyTimes(times);
717        zoomToFactor(x, y, newScale / oldScale);
718    }
719
720    public void zoomToFactor(double x, double y, double factor) {
721        double newScale = getScale()*factor;
722        EastNorth oldUnderMouse = getState().getForView(x, y).getEastNorth();
723        MapViewState newState = getState().usingScale(newScale);
724        newState = newState.movedTo(newState.getForView(x, y), oldUnderMouse);
725        zoomTo(newState.getCenter().getEastNorth(), newScale);
726    }
727
728    public void zoomToFactor(EastNorth newCenter, double factor) {
729        zoomTo(newCenter, getScale()*factor);
730    }
731
732    public void zoomToFactor(double factor) {
733        zoomTo(getCenter(), getScale()*factor);
734    }
735
736    /**
737     * Zoom to given projection bounds.
738     * @param box new projection bounds
739     */
740    public void zoomTo(ProjectionBounds box) {
741        // -20 to leave some border
742        int w = getWidth()-20;
743        if (w < 20) {
744            w = 20;
745        }
746        int h = getHeight()-20;
747        if (h < 20) {
748            h = 20;
749        }
750
751        double scaleX = (box.maxEast-box.minEast)/w;
752        double scaleY = (box.maxNorth-box.minNorth)/h;
753        double newScale = Math.max(scaleX, scaleY);
754
755        newScale = scaleFloor(newScale);
756        zoomTo(box.getCenter(), newScale);
757    }
758
759    /**
760     * Zoom to given bounds.
761     * @param box new bounds
762     */
763    public void zoomTo(Bounds box) {
764        zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
765                getProjection().latlon2eastNorth(box.getMax())));
766    }
767
768    /**
769     * Zoom to given viewport data.
770     * @param viewport new viewport data
771     */
772    public void zoomTo(ViewportData viewport) {
773        if (viewport == null) return;
774        if (viewport.getBounds() != null) {
775            BoundingXYVisitor box = new BoundingXYVisitor();
776            box.visit(viewport.getBounds());
777            zoomTo(box);
778        } else {
779            zoomTo(viewport.getCenter(), viewport.getScale(), true);
780        }
781    }
782
783    /**
784     * Set the new dimension to the view.
785     * @param box box to zoom to
786     */
787    public void zoomTo(BoundingXYVisitor box) {
788        if (box == null) {
789            box = new BoundingXYVisitor();
790        }
791        if (box.getBounds() == null) {
792            box.visit(getProjection().getWorldBoundsLatLon());
793        }
794        if (!box.hasExtend()) {
795            box.enlargeBoundingBox();
796        }
797
798        zoomTo(box.getBounds());
799    }
800
801    private static class ZoomData {
802        private final EastNorth center;
803        private final double scale;
804
805        ZoomData(EastNorth center, double scale) {
806            this.center = center;
807            this.scale = scale;
808        }
809
810        public EastNorth getCenterEastNorth() {
811            return center;
812        }
813
814        public double getScale() {
815            return scale;
816        }
817    }
818
819    private final transient Stack<ZoomData> zoomUndoBuffer = new Stack<>();
820    private final transient Stack<ZoomData> zoomRedoBuffer = new Stack<>();
821    private Date zoomTimestamp = new Date();
822
823    private void pushZoomUndo(EastNorth center, double scale) {
824        Date now = new Date();
825        if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {
826            zoomUndoBuffer.push(new ZoomData(center, scale));
827            if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {
828                zoomUndoBuffer.remove(0);
829            }
830            zoomRedoBuffer.clear();
831        }
832        zoomTimestamp = now;
833    }
834
835    /**
836     * Zoom to previous location.
837     */
838    public void zoomPrevious() {
839        if (!zoomUndoBuffer.isEmpty()) {
840            ZoomData zoom = zoomUndoBuffer.pop();
841            zoomRedoBuffer.push(new ZoomData(getCenter(), getScale()));
842            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
843        }
844    }
845
846    /**
847     * Zoom to next location.
848     */
849    public void zoomNext() {
850        if (!zoomRedoBuffer.isEmpty()) {
851            ZoomData zoom = zoomRedoBuffer.pop();
852            zoomUndoBuffer.push(new ZoomData(getCenter(), getScale()));
853            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
854        }
855    }
856
857    /**
858     * Determines if zoom history contains "undo" entries.
859     * @return {@code true} if zoom history contains "undo" entries
860     */
861    public boolean hasZoomUndoEntries() {
862        return !zoomUndoBuffer.isEmpty();
863    }
864
865    /**
866     * Determines if zoom history contains "redo" entries.
867     * @return {@code true} if zoom history contains "redo" entries
868     */
869    public boolean hasZoomRedoEntries() {
870        return !zoomRedoBuffer.isEmpty();
871    }
872
873    private BBox getBBox(Point p, int snapDistance) {
874        return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
875                getLatLon(p.x + snapDistance, p.y + snapDistance));
876    }
877
878    /**
879     * The *result* does not depend on the current map selection state, neither does the result *order*.
880     * It solely depends on the distance to point p.
881     * @param p point
882     * @param predicate predicate to match
883     *
884     * @return a sorted map with the keys representing the distance of their associated nodes to point p.
885     */
886    private Map<Double, List<Node>> getNearestNodesImpl(Point p, Predicate<OsmPrimitive> predicate) {
887        Map<Double, List<Node>> nearestMap = new TreeMap<>();
888        DataSet ds = Main.getLayerManager().getEditDataSet();
889
890        if (ds != null) {
891            double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
892            snapDistanceSq *= snapDistanceSq;
893
894            for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
895                if (predicate.test(n)
896                        && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
897                    List<Node> nlist;
898                    if (nearestMap.containsKey(dist)) {
899                        nlist = nearestMap.get(dist);
900                    } else {
901                        nlist = new LinkedList<>();
902                        nearestMap.put(dist, nlist);
903                    }
904                    nlist.add(n);
905                }
906            }
907        }
908
909        return nearestMap;
910    }
911
912    /**
913     * The *result* does not depend on the current map selection state,
914     * neither does the result *order*.
915     * It solely depends on the distance to point p.
916     *
917     * @param p the point for which to search the nearest segment.
918     * @param ignore a collection of nodes which are not to be returned.
919     * @param predicate the returned objects have to fulfill certain properties.
920     *
921     * @return All nodes nearest to point p that are in a belt from
922     *      dist(nearest) to dist(nearest)+4px around p and
923     *      that are not in ignore.
924     */
925    public final List<Node> getNearestNodes(Point p,
926            Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
927        List<Node> nearestList = Collections.emptyList();
928
929        if (ignore == null) {
930            ignore = Collections.emptySet();
931        }
932
933        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
934        if (!nlists.isEmpty()) {
935            Double minDistSq = null;
936            for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
937                Double distSq = entry.getKey();
938                List<Node> nlist = entry.getValue();
939
940                // filter nodes to be ignored before determining minDistSq..
941                nlist.removeAll(ignore);
942                if (minDistSq == null) {
943                    if (!nlist.isEmpty()) {
944                        minDistSq = distSq;
945                        nearestList = new ArrayList<>();
946                        nearestList.addAll(nlist);
947                    }
948                } else {
949                    if (distSq-minDistSq < (4)*(4)) {
950                        nearestList.addAll(nlist);
951                    }
952                }
953            }
954        }
955
956        return nearestList;
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 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.
969     * @see #getNearestNodes(Point, Collection, Predicate)
970     */
971    public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
972        return getNearestNodes(p, null, predicate);
973    }
974
975    /**
976     * The *result* depends on the current map selection state IF use_selected is true.
977     *
978     * If more than one node within node.snap-distance pixels is found,
979     * the nearest node selected is returned IF use_selected is true.
980     *
981     * Else the nearest new/id=0 node within about the same distance
982     * as the true nearest node is returned.
983     *
984     * If no such node is found either, the true nearest node to p is returned.
985     *
986     * Finally, if a node is not found at all, null is returned.
987     *
988     * @param p the screen point
989     * @param predicate this parameter imposes a condition on the returned object, e.g.
990     *        give the nearest node that is tagged.
991     * @param useSelected make search depend on selection
992     *
993     * @return A node within snap-distance to point p, that is chosen by the algorithm described.
994     */
995    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
996        return getNearestNode(p, predicate, useSelected, null);
997    }
998
999    /**
1000     * The *result* depends on the current map selection state IF use_selected is true
1001     *
1002     * If more than one node within node.snap-distance pixels is found,
1003     * the nearest node selected is returned IF use_selected is true.
1004     *
1005     * If there are no selected nodes near that point, the node that is related to some of the preferredRefs
1006     *
1007     * Else the nearest new/id=0 node within about the same distance
1008     * as the true nearest node is returned.
1009     *
1010     * If no such node is found either, the true nearest node to p is returned.
1011     *
1012     * Finally, if a node is not found at all, null is returned.
1013     *
1014     * @param p the screen point
1015     * @param predicate this parameter imposes a condition on the returned object, e.g.
1016     *        give the nearest node that is tagged.
1017     * @param useSelected make search depend on selection
1018     * @param preferredRefs primitives, whose nodes we prefer
1019     *
1020     * @return A node within snap-distance to point p, that is chosen by the algorithm described.
1021     * @since 6065
1022     */
1023    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
1024            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1025
1026        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
1027        if (nlists.isEmpty()) return null;
1028
1029        if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
1030        Node ntsel = null, ntnew = null, ntref = null;
1031        boolean useNtsel = useSelected;
1032        double minDistSq = nlists.keySet().iterator().next();
1033
1034        for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
1035            Double distSq = entry.getKey();
1036            for (Node nd : entry.getValue()) {
1037                // find the nearest selected node
1038                if (ntsel == null && nd.isSelected()) {
1039                    ntsel = nd;
1040                    // if there are multiple nearest nodes, prefer the one
1041                    // that is selected. This is required in order to drag
1042                    // the selected node if multiple nodes have the same
1043                    // coordinates (e.g. after unglue)
1044                    useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
1045                }
1046                if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
1047                    List<OsmPrimitive> ndRefs = nd.getReferrers();
1048                    for (OsmPrimitive ref: preferredRefs) {
1049                        if (ndRefs.contains(ref)) {
1050                            ntref = nd;
1051                            break;
1052                        }
1053                    }
1054                }
1055                // find the nearest newest node that is within about the same
1056                // distance as the true nearest node
1057                if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
1058                    ntnew = nd;
1059                }
1060            }
1061        }
1062
1063        // take nearest selected, nearest new or true nearest node to p, in that order
1064        if (ntsel != null && useNtsel)
1065            return ntsel;
1066        if (ntref != null)
1067            return ntref;
1068        if (ntnew != null)
1069            return ntnew;
1070        return nlists.values().iterator().next().get(0);
1071    }
1072
1073    /**
1074     * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
1075     * @param p the screen point
1076     * @param predicate this parameter imposes a condition on the returned object, e.g.
1077     *        give the nearest node that is tagged.
1078     *
1079     * @return The nearest node to point p.
1080     */
1081    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
1082        return getNearestNode(p, predicate, true);
1083    }
1084
1085    /**
1086     * The *result* does not depend on the current map selection state, neither does the result *order*.
1087     * It solely depends on the distance to point p.
1088     * @param p the screen point
1089     * @param predicate this parameter imposes a condition on the returned object, e.g.
1090     *        give the nearest node that is tagged.
1091     *
1092     * @return a sorted map with the keys representing the perpendicular
1093     *      distance of their associated way segments to point p.
1094     */
1095    private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
1096        Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
1097        DataSet ds = Main.getLayerManager().getEditDataSet();
1098
1099        if (ds != null) {
1100            double snapDistanceSq = Main.pref.getInteger("mappaint.segment.snap-distance", 10);
1101            snapDistanceSq *= snapDistanceSq;
1102
1103            for (Way w : ds.searchWays(getBBox(p, Main.pref.getInteger("mappaint.segment.snap-distance", 10)))) {
1104                if (!predicate.test(w)) {
1105                    continue;
1106                }
1107                Node lastN = null;
1108                int i = -2;
1109                for (Node n : w.getNodes()) {
1110                    i++;
1111                    if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
1112                        continue;
1113                    }
1114                    if (lastN == null) {
1115                        lastN = n;
1116                        continue;
1117                    }
1118
1119                    Point2D pA = getPoint2D(lastN);
1120                    Point2D pB = getPoint2D(n);
1121                    double c = pA.distanceSq(pB);
1122                    double a = p.distanceSq(pB);
1123                    double b = p.distanceSq(pA);
1124
1125                    /* perpendicular distance squared
1126                     * loose some precision to account for possible deviations in the calculation above
1127                     * e.g. if identical (A and B) come about reversed in another way, values may differ
1128                     * -- zero out least significant 32 dual digits of mantissa..
1129                     */
1130                    double perDistSq = Double.longBitsToDouble(
1131                            Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c)
1132                            >> 32 << 32); // resolution in numbers with large exponent not needed here..
1133
1134                    if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
1135                        List<WaySegment> wslist;
1136                        if (nearestMap.containsKey(perDistSq)) {
1137                            wslist = nearestMap.get(perDistSq);
1138                        } else {
1139                            wslist = new LinkedList<>();
1140                            nearestMap.put(perDistSq, wslist);
1141                        }
1142                        wslist.add(new WaySegment(w, i));
1143                    }
1144
1145                    lastN = n;
1146                }
1147            }
1148        }
1149
1150        return nearestMap;
1151    }
1152
1153    /**
1154     * The result *order* depends on the current map selection state.
1155     * Segments within 10px of p are searched and sorted by their distance to @param p,
1156     * then, within groups of equally distant segments, prefer those that are selected.
1157     *
1158     * @param p the point for which to search the nearest segments.
1159     * @param ignore a collection of segments which are not to be returned.
1160     * @param predicate the returned objects have to fulfill certain properties.
1161     *
1162     * @return all segments within 10px of p that are not in ignore,
1163     *          sorted by their perpendicular distance.
1164     */
1165    public final List<WaySegment> getNearestWaySegments(Point p,
1166            Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
1167        List<WaySegment> nearestList = new ArrayList<>();
1168        List<WaySegment> unselected = new LinkedList<>();
1169
1170        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1171            // put selected waysegs within each distance group first
1172            // makes the order of nearestList dependent on current selection state
1173            for (WaySegment ws : wss) {
1174                (ws.way.isSelected() ? nearestList : unselected).add(ws);
1175            }
1176            nearestList.addAll(unselected);
1177            unselected.clear();
1178        }
1179        if (ignore != null) {
1180            nearestList.removeAll(ignore);
1181        }
1182
1183        return nearestList;
1184    }
1185
1186    /**
1187     * The result *order* depends on the current map selection state.
1188     *
1189     * @param p the point for which to search the nearest segments.
1190     * @param predicate the returned objects have to fulfill certain properties.
1191     *
1192     * @return all segments within 10px of p, sorted by their perpendicular distance.
1193     * @see #getNearestWaySegments(Point, Collection, Predicate)
1194     */
1195    public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
1196        return getNearestWaySegments(p, null, predicate);
1197    }
1198
1199    /**
1200     * The *result* depends on the current map selection state IF use_selected is true.
1201     *
1202     * @param p the point for which to search the nearest segment.
1203     * @param predicate the returned object has to fulfill certain properties.
1204     * @param useSelected whether selected way segments should be preferred.
1205     *
1206     * @return The nearest way segment to point p,
1207     *      and, depending on use_selected, prefers a selected way segment, if found.
1208     * @see #getNearestWaySegments(Point, Collection, Predicate)
1209     */
1210    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1211        WaySegment wayseg = null;
1212        WaySegment ntsel = null;
1213
1214        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1215            if (wayseg != null && ntsel != null) {
1216                break;
1217            }
1218            for (WaySegment ws : wslist) {
1219                if (wayseg == null) {
1220                    wayseg = ws;
1221                }
1222                if (ntsel == null && ws.way.isSelected()) {
1223                    ntsel = ws;
1224                }
1225            }
1226        }
1227
1228        return (ntsel != null && useSelected) ? ntsel : wayseg;
1229    }
1230
1231    /**
1232     * The *result* depends on the current map selection state IF use_selected is true.
1233     *
1234     * @param p the point for which to search the nearest segment.
1235     * @param predicate the returned object has to fulfill certain properties.
1236     * @param useSelected whether selected way segments should be preferred.
1237     * @param preferredRefs - prefer segments related to these primitives, may be null
1238     *
1239     * @return The nearest way segment to point p,
1240     *      and, depending on use_selected, prefers a selected way segment, if found.
1241     * Also prefers segments of ways that are related to one of preferredRefs primitives
1242     *
1243     * @see #getNearestWaySegments(Point, Collection, Predicate)
1244     * @since 6065
1245     */
1246    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
1247            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
1248        WaySegment wayseg = null;
1249        WaySegment ntsel = null;
1250        WaySegment ntref = null;
1251        if (preferredRefs != null && preferredRefs.isEmpty())
1252            preferredRefs = null;
1253
1254        searchLoop: for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
1255            for (WaySegment ws : wslist) {
1256                if (wayseg == null) {
1257                    wayseg = ws;
1258                }
1259                if (ntsel == null && ws.way.isSelected()) {
1260                    ntsel = ws;
1261                    break searchLoop;
1262                }
1263                if (ntref == null && preferredRefs != null) {
1264                    // prefer ways containing given nodes
1265                    for (Node nd: ws.way.getNodes()) {
1266                        if (preferredRefs.contains(nd)) {
1267                            ntref = ws;
1268                            break searchLoop;
1269                        }
1270                    }
1271                    Collection<OsmPrimitive> wayRefs = ws.way.getReferrers();
1272                    // prefer member of the given relations
1273                    for (OsmPrimitive ref: preferredRefs) {
1274                        if (ref instanceof Relation && wayRefs.contains(ref)) {
1275                            ntref = ws;
1276                            break searchLoop;
1277                        }
1278                    }
1279                }
1280            }
1281        }
1282        if (ntsel != null && useSelected)
1283            return ntsel;
1284        if (ntref != null)
1285            return ntref;
1286        return wayseg;
1287    }
1288
1289    /**
1290     * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
1291     * @param p the point for which to search the nearest segment.
1292     * @param predicate the returned object has to fulfill certain properties.
1293     *
1294     * @return The nearest way segment to point p.
1295     */
1296    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
1297        return getNearestWaySegment(p, predicate, true);
1298    }
1299
1300    /**
1301     * The *result* does not depend on the current map selection state,
1302     * neither does the result *order*.
1303     * It solely depends on the perpendicular distance to point p.
1304     *
1305     * @param p the point for which to search the nearest ways.
1306     * @param ignore a collection of ways which are not to be returned.
1307     * @param predicate the returned object has to fulfill certain properties.
1308     *
1309     * @return all nearest ways to the screen point given that are not in ignore.
1310     * @see #getNearestWaySegments(Point, Collection, Predicate)
1311     */
1312    public final List<Way> getNearestWays(Point p,
1313            Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
1314        List<Way> nearestList = new ArrayList<>();
1315        Set<Way> wset = new HashSet<>();
1316
1317        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1318            for (WaySegment ws : wss) {
1319                if (wset.add(ws.way)) {
1320                    nearestList.add(ws.way);
1321                }
1322            }
1323        }
1324        if (ignore != null) {
1325            nearestList.removeAll(ignore);
1326        }
1327
1328        return nearestList;
1329    }
1330
1331    /**
1332     * The *result* does not depend on the current map selection state,
1333     * neither does the result *order*.
1334     * It solely depends on the perpendicular distance to point p.
1335     *
1336     * @param p the point for which to search the nearest ways.
1337     * @param predicate the returned object has to fulfill certain properties.
1338     *
1339     * @return all nearest ways to the screen point given.
1340     * @see #getNearestWays(Point, Collection, Predicate)
1341     */
1342    public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1343        return getNearestWays(p, null, predicate);
1344    }
1345
1346    /**
1347     * The *result* depends on the current map selection state.
1348     *
1349     * @param p the point for which to search the nearest segment.
1350     * @param predicate the returned object has to fulfill certain properties.
1351     *
1352     * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
1353     * @see #getNearestWaySegment(Point, Predicate)
1354     */
1355    public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
1356        WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
1357        return (nearestWaySeg == null) ? null : nearestWaySeg.way;
1358    }
1359
1360    /**
1361     * The *result* does not depend on the current map selection state,
1362     * neither does the result *order*.
1363     * It solely depends on the distance to point p.
1364     *
1365     * First, nodes will be searched. If there are nodes within BBox found,
1366     * return a collection of those nodes only.
1367     *
1368     * If no nodes are found, search for nearest ways. If there are ways
1369     * within BBox found, return a collection of those ways only.
1370     *
1371     * If nothing is found, return an empty collection.
1372     *
1373     * @param p The point on screen.
1374     * @param ignore a collection of ways which are not to be returned.
1375     * @param predicate the returned object has to fulfill certain properties.
1376     *
1377     * @return Primitives nearest to the given screen point that are not in ignore.
1378     * @see #getNearestNodes(Point, Collection, Predicate)
1379     * @see #getNearestWays(Point, Collection, Predicate)
1380     */
1381    public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
1382            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1383        List<OsmPrimitive> nearestList = Collections.emptyList();
1384        OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
1385
1386        if (osm != null) {
1387            if (osm instanceof Node) {
1388                nearestList = new ArrayList<>(getNearestNodes(p, predicate));
1389            } else if (osm instanceof Way) {
1390                nearestList = new ArrayList<>(getNearestWays(p, predicate));
1391            }
1392            if (ignore != null) {
1393                nearestList.removeAll(ignore);
1394            }
1395        }
1396
1397        return nearestList;
1398    }
1399
1400    /**
1401     * The *result* does not depend on the current map selection state,
1402     * neither does the result *order*.
1403     * It solely depends on the distance to point p.
1404     *
1405     * @param p The point on screen.
1406     * @param predicate the returned object has to fulfill certain properties.
1407     * @return Primitives nearest to the given screen point.
1408     * @see #getNearestNodesOrWays(Point, Collection, Predicate)
1409     */
1410    public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1411        return getNearestNodesOrWays(p, null, predicate);
1412    }
1413
1414    /**
1415     * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
1416     * It decides, whether to yield the node to be tested or look for further (way) candidates.
1417     *
1418     * @param osm node to check
1419     * @param p point clicked
1420     * @param useSelected whether to prefer selected nodes
1421     * @return true, if the node fulfills the properties of the function body
1422     */
1423    private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
1424        if (osm != null) {
1425            if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
1426            if (osm.isTagged()) return true;
1427            if (useSelected && osm.isSelected()) return true;
1428        }
1429        return false;
1430    }
1431
1432    /**
1433     * The *result* depends on the current map selection state IF use_selected is true.
1434     *
1435     * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
1436     * the nearest, selected node.  If not found, try {@link #getNearestWaySegment(Point, Predicate)}
1437     * to find the nearest selected way.
1438     *
1439     * IF use_selected is false, or if no selected primitive was found, do the following.
1440     *
1441     * If the nearest node found is within 4px of p, simply take it.
1442     * Else, find the nearest way segment. Then, if p is closer to its
1443     * middle than to the node, take the way segment, else take the node.
1444     *
1445     * Finally, if no nearest primitive is found at all, return null.
1446     *
1447     * @param p The point on screen.
1448     * @param predicate the returned object has to fulfill certain properties.
1449     * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
1450     *
1451     * @return A primitive within snap-distance to point p,
1452     *      that is chosen by the algorithm described.
1453     * @see #getNearestNode(Point, Predicate)
1454     * @see #getNearestWay(Point, Predicate)
1455     */
1456    public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
1457        Collection<OsmPrimitive> sel;
1458        DataSet ds = Main.getLayerManager().getEditDataSet();
1459        if (useSelected && ds != null) {
1460            sel = ds.getSelected();
1461        } else {
1462            sel = null;
1463        }
1464        OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
1465
1466        if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
1467        WaySegment ws;
1468        if (useSelected) {
1469            ws = getNearestWaySegment(p, predicate, useSelected, sel);
1470        } else {
1471            ws = getNearestWaySegment(p, predicate, useSelected);
1472        }
1473        if (ws == null) return osm;
1474
1475        if ((ws.way.isSelected() && useSelected) || osm == null) {
1476            // either (no _selected_ nearest node found, if desired) or no nearest node was found
1477            osm = ws.way;
1478        } else {
1479            int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1480            maxWaySegLenSq *= maxWaySegLenSq;
1481
1482            Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
1483            Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
1484
1485            // is wayseg shorter than maxWaySegLenSq and
1486            // is p closer to the middle of wayseg  than  to the nearest node?
1487            if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1488                    p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
1489                osm = ws.way;
1490            }
1491        }
1492        return osm;
1493    }
1494
1495    /**
1496     * if r = 0 returns a, if r=1 returns b,
1497     * if r = 0.5 returns center between a and b, etc..
1498     *
1499     * @param r scale value
1500     * @param a root of vector
1501     * @param b vector
1502     * @return new point at a + r*(ab)
1503     */
1504    public static Point2D project(double r, Point2D a, Point2D b) {
1505        Point2D ret = null;
1506
1507        if (a != null && b != null) {
1508            ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1509                    a.getY() + r*(b.getY()-a.getY()));
1510        }
1511        return ret;
1512    }
1513
1514    /**
1515     * The *result* does not depend on the current map selection state, neither does the result *order*.
1516     * It solely depends on the distance to point p.
1517     *
1518     * @param p The point on screen.
1519     * @param ignore a collection of ways which are not to be returned.
1520     * @param predicate the returned object has to fulfill certain properties.
1521     *
1522     * @return a list of all objects that are nearest to point p and
1523     *          not in ignore or an empty list if nothing was found.
1524     */
1525    public final List<OsmPrimitive> getAllNearest(Point p,
1526            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1527        List<OsmPrimitive> nearestList = new ArrayList<>();
1528        Set<Way> wset = new HashSet<>();
1529
1530        // add nearby ways
1531        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1532            for (WaySegment ws : wss) {
1533                if (wset.add(ws.way)) {
1534                    nearestList.add(ws.way);
1535                }
1536            }
1537        }
1538
1539        // add nearby nodes
1540        for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
1541            nearestList.addAll(nlist);
1542        }
1543
1544        // add parent relations of nearby nodes and ways
1545        Set<OsmPrimitive> parentRelations = new HashSet<>();
1546        for (OsmPrimitive o : nearestList) {
1547            for (OsmPrimitive r : o.getReferrers()) {
1548                if (r instanceof Relation && predicate.test(r)) {
1549                    parentRelations.add(r);
1550                }
1551            }
1552        }
1553        nearestList.addAll(parentRelations);
1554
1555        if (ignore != null) {
1556            nearestList.removeAll(ignore);
1557        }
1558
1559        return nearestList;
1560    }
1561
1562    /**
1563     * The *result* does not depend on the current map selection state, neither does the result *order*.
1564     * It solely depends on the distance to point p.
1565     *
1566     * @param p The point on screen.
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
1570     *          or an empty list if nothing was found.
1571     * @see #getAllNearest(Point, Collection, Predicate)
1572     */
1573    public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1574        return getAllNearest(p, null, predicate);
1575    }
1576
1577    /**
1578     * @return The projection to be used in calculating stuff.
1579     */
1580    public Projection getProjection() {
1581        return state.getProjection();
1582    }
1583
1584    @Override
1585    public String helpTopic() {
1586        String n = getClass().getName();
1587        return n.substring(n.lastIndexOf('.')+1);
1588    }
1589
1590    /**
1591     * Return a ID which is unique as long as viewport dimensions are the same
1592     * @return A unique ID, as long as viewport dimensions are the same
1593     */
1594    public int getViewID() {
1595        EastNorth center = getCenter();
1596        String x = new StringBuilder().append(center.east())
1597                          .append('_').append(center.north())
1598                          .append('_').append(getScale())
1599                          .append('_').append(getWidth())
1600                          .append('_').append(getHeight())
1601                          .append('_').append(getProjection()).toString();
1602        CRC32 id = new CRC32();
1603        id.update(x.getBytes(StandardCharsets.UTF_8));
1604        return (int) id.getValue();
1605    }
1606
1607    /**
1608     * Set new cursor.
1609     * @param cursor The new cursor to use.
1610     * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1611     */
1612    public void setNewCursor(Cursor cursor, Object reference) {
1613        cursorManager.setNewCursor(cursor, reference);
1614    }
1615
1616    /**
1617     * Set new cursor.
1618     * @param cursor the type of predefined cursor
1619     * @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
1620     */
1621    public void setNewCursor(int cursor, Object reference) {
1622        setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1623    }
1624
1625    /**
1626     * Remove the new cursor and reset to previous
1627     * @param reference Cursor reference
1628     */
1629    public void resetCursor(Object reference) {
1630        cursorManager.resetCursor(reference);
1631    }
1632
1633    /**
1634     * Gets the cursor manager that is used for this NavigatableComponent.
1635     * @return The cursor manager.
1636     */
1637    public CursorManager getCursorManager() {
1638        return cursorManager;
1639    }
1640
1641    /**
1642     * Get a max scale for projection that describes world in 1/512 of the projection unit
1643     * @return max scale
1644     */
1645    public double getMaxScale() {
1646        ProjectionBounds world = getMaxProjectionBounds();
1647        return Math.max(
1648            world.maxNorth-world.minNorth,
1649            world.maxEast-world.minEast
1650        )/512;
1651    }
1652}
Note: See TracBrowser for help on using the repository browser.