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