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

Last change on this file since 13963 was 13868, checked in by Don-vip, 6 years ago

spotbugs - DLS_DEAD_LOCAL_STORE

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