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