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

Last change on this file since 12818 was 12778, checked in by bastiK, 7 years ago

see #15229 - deprecate Projections#project and Projections#inverseProject

replacement is a bit more verbose, but the fact that Main.proj is
involved need not be hidden

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