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

Last change on this file since 17356 was 16671, checked in by stoecker, 4 years ago

fix #19402 - fix race condition in auto follow function

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