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