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