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