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

Last change on this file since 12116 was 12074, checked in by bastiK, 7 years ago

fixed #14740 - Current displayed data position changes when you press TAB

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