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

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

fix #13883 - statusbar is not updated when the map is moved with keyboard (ctrl + arrow keys)

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