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

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

sonar - squid:S2142 - "InterruptedException" should not be ignored

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