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

Last change on this file since 12511 was 12163, checked in by michael2402, 7 years ago

Make use of ILatLon#getEastNorth

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