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

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

see #15229 - deprecate all Main methods related to projections. New ProjectionRegistry class

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