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