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

Last change on this file since 12670 was 12636, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.getLayerManager(). Replacement: gui.MainApplication.getLayerManager()

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