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

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

fix #15599 - Improvements for testing painting (last patches by ris)

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