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

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

see #8039, see #10456 - support read-only data layers

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