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

Last change on this file since 13036 was 12846, checked in by bastiK, 7 years ago

see #15229 - use Config.getPref() wherever possible

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