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

Last change on this file since 12136 was 12119, checked in by michael2402, 7 years ago

Fix weak projection listener of NavigatableComponent being lost.

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