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