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 dm = ll1.greatCircleDistance(ll2); 556 double den = 100 * scale; 557 double scaleMin = 0.01 * den / dm / 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(1000L / 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 pA = getPoint2D(lastN); 1029 Point2D pB = getPoint2D(n); 1030 double c = pA.distanceSq(pB); 1031 double a = p.distanceSq(pB); 1032 double b = p.distanceSq(pA); 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; 1121 WaySegment ntsel = null; 1122 1123 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) { 1124 if (wayseg != null && ntsel != null) { 1125 break; 1126 } 1127 for (WaySegment ws : wslist) { 1128 if (wayseg == null) { 1129 wayseg = ws; 1130 } 1131 if (ntsel == null && ws.way.isSelected()) { 1132 ntsel = ws; 1133 } 1134 } 1135 } 1136 1137 return (ntsel != null && useSelected) ? ntsel : wayseg; 1138 } 1139 1140 /** 1141 * The *result* depends on the current map selection state IF use_selected is true. 1142 * 1143 * @param p the point for which to search the nearest segment. 1144 * @param predicate the returned object has to fulfill certain properties. 1145 * @param useSelected whether selected way segments should be preferred. 1146 * @param preferredRefs - prefer segments related to these primitives, may be null 1147 * 1148 * @return The nearest way segment to point p, 1149 * and, depending on use_selected, prefers a selected way segment, if found. 1150 * Also prefers segments of ways that are related to one of preferredRefs primitives 1151 * 1152 * @see #getNearestWaySegments(Point, Collection, Predicate) 1153 * @since 6065 1154 */ 1155 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, 1156 boolean useSelected, Collection<OsmPrimitive> preferredRefs) { 1157 WaySegment wayseg = null; 1158 WaySegment ntsel = null; 1159 WaySegment ntref = null; 1160 if (preferredRefs != null && preferredRefs.isEmpty()) 1161 preferredRefs = null; 1162 1163 searchLoop: for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) { 1164 for (WaySegment ws : wslist) { 1165 if (wayseg == null) { 1166 wayseg = ws; 1167 } 1168 if (ntsel == null && ws.way.isSelected()) { 1169 ntsel = ws; 1170 break searchLoop; 1171 } 1172 if (ntref == null && preferredRefs != null) { 1173 // prefer ways containing given nodes 1174 for (Node nd: ws.way.getNodes()) { 1175 if (preferredRefs.contains(nd)) { 1176 ntref = ws; 1177 break searchLoop; 1178 } 1179 } 1180 Collection<OsmPrimitive> wayRefs = ws.way.getReferrers(); 1181 // prefer member of the given relations 1182 for (OsmPrimitive ref: preferredRefs) { 1183 if (ref instanceof Relation && wayRefs.contains(ref)) { 1184 ntref = ws; 1185 break searchLoop; 1186 } 1187 } 1188 } 1189 } 1190 } 1191 if (ntsel != null && useSelected) 1192 return ntsel; 1193 if (ntref != null) 1194 return ntref; 1195 return wayseg; 1196 } 1197 1198 /** 1199 * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}. 1200 * @param p the point for which to search the nearest segment. 1201 * @param predicate the returned object has to fulfill certain properties. 1202 * 1203 * @return The nearest way segment to point p. 1204 */ 1205 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) { 1206 return getNearestWaySegment(p, predicate, true); 1207 } 1208 1209 /** 1210 * The *result* does not depend on the current map selection state, 1211 * neither does the result *order*. 1212 * It solely depends on the perpendicular distance to point p. 1213 * 1214 * @param p the point for which to search the nearest ways. 1215 * @param ignore a collection of ways which are not to be returned. 1216 * @param predicate the returned object has to fulfill certain properties. 1217 * 1218 * @return all nearest ways to the screen point given that are not in ignore. 1219 * @see #getNearestWaySegments(Point, Collection, Predicate) 1220 */ 1221 public final List<Way> getNearestWays(Point p, 1222 Collection<Way> ignore, Predicate<OsmPrimitive> predicate) { 1223 List<Way> nearestList = new ArrayList<>(); 1224 Set<Way> wset = new HashSet<>(); 1225 1226 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { 1227 for (WaySegment ws : wss) { 1228 if (wset.add(ws.way)) { 1229 nearestList.add(ws.way); 1230 } 1231 } 1232 } 1233 if (ignore != null) { 1234 nearestList.removeAll(ignore); 1235 } 1236 1237 return nearestList; 1238 } 1239 1240 /** 1241 * The *result* does not depend on the current map selection state, 1242 * neither does the result *order*. 1243 * It solely depends on the perpendicular distance to point p. 1244 * 1245 * @param p the point for which to search the nearest ways. 1246 * @param predicate the returned object has to fulfill certain properties. 1247 * 1248 * @return all nearest ways to the screen point given. 1249 * @see #getNearestWays(Point, Collection, Predicate) 1250 */ 1251 public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) { 1252 return getNearestWays(p, null, predicate); 1253 } 1254 1255 /** 1256 * The *result* depends on the current map selection state. 1257 * 1258 * @param p the point for which to search the nearest segment. 1259 * @param predicate the returned object has to fulfill certain properties. 1260 * 1261 * @return The nearest way to point p, prefer a selected way if there are multiple nearest. 1262 * @see #getNearestWaySegment(Point, Predicate) 1263 */ 1264 public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) { 1265 WaySegment nearestWaySeg = getNearestWaySegment(p, predicate); 1266 return (nearestWaySeg == null) ? null : nearestWaySeg.way; 1267 } 1268 1269 /** 1270 * The *result* does not depend on the current map selection state, 1271 * neither does the result *order*. 1272 * It solely depends on the distance to point p. 1273 * 1274 * First, nodes will be searched. If there are nodes within BBox found, 1275 * return a collection of those nodes only. 1276 * 1277 * If no nodes are found, search for nearest ways. If there are ways 1278 * within BBox found, return a collection of those ways only. 1279 * 1280 * If nothing is found, return an empty collection. 1281 * 1282 * @param p The point on screen. 1283 * @param ignore a collection of ways which are not to be returned. 1284 * @param predicate the returned object has to fulfill certain properties. 1285 * 1286 * @return Primitives nearest to the given screen point that are not in ignore. 1287 * @see #getNearestNodes(Point, Collection, Predicate) 1288 * @see #getNearestWays(Point, Collection, Predicate) 1289 */ 1290 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, 1291 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) { 1292 List<OsmPrimitive> nearestList = Collections.emptyList(); 1293 OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false); 1294 1295 if (osm != null) { 1296 if (osm instanceof Node) { 1297 nearestList = new ArrayList<OsmPrimitive>(getNearestNodes(p, predicate)); 1298 } else if (osm instanceof Way) { 1299 nearestList = new ArrayList<OsmPrimitive>(getNearestWays(p, predicate)); 1300 } 1301 if (ignore != null) { 1302 nearestList.removeAll(ignore); 1303 } 1304 } 1305 1306 return nearestList; 1307 } 1308 1309 /** 1310 * The *result* does not depend on the current map selection state, 1311 * neither does the result *order*. 1312 * It solely depends on the distance to point p. 1313 * 1314 * @param p The point on screen. 1315 * @param predicate the returned object has to fulfill certain properties. 1316 * @return Primitives nearest to the given screen point. 1317 * @see #getNearestNodesOrWays(Point, Collection, Predicate) 1318 */ 1319 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) { 1320 return getNearestNodesOrWays(p, null, predicate); 1321 } 1322 1323 /** 1324 * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)} 1325 * It decides, whether to yield the node to be tested or look for further (way) candidates. 1326 * 1327 * @param osm node to check 1328 * @param p point clicked 1329 * @param useSelected whether to prefer selected nodes 1330 * @return true, if the node fulfills the properties of the function body 1331 */ 1332 private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) { 1333 if (osm != null) { 1334 if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true; 1335 if (osm.isTagged()) return true; 1336 if (useSelected && osm.isSelected()) return true; 1337 } 1338 return false; 1339 } 1340 1341 /** 1342 * The *result* depends on the current map selection state IF use_selected is true. 1343 * 1344 * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find 1345 * the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)} 1346 * to find the nearest selected way. 1347 * 1348 * IF use_selected is false, or if no selected primitive was found, do the following. 1349 * 1350 * If the nearest node found is within 4px of p, simply take it. 1351 * Else, find the nearest way segment. Then, if p is closer to its 1352 * middle than to the node, take the way segment, else take the node. 1353 * 1354 * Finally, if no nearest primitive is found at all, return null. 1355 * 1356 * @param p The point on screen. 1357 * @param predicate the returned object has to fulfill certain properties. 1358 * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives 1359 * 1360 * @return A primitive within snap-distance to point p, 1361 * that is chosen by the algorithm described. 1362 * @see #getNearestNode(Point, Predicate) 1363 * @see #getNearestWay(Point, Predicate) 1364 */ 1365 public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) { 1366 Collection<OsmPrimitive> sel; 1367 DataSet ds = getCurrentDataSet(); 1368 if (useSelected && ds != null) { 1369 sel = ds.getSelected(); 1370 } else { 1371 sel = null; 1372 } 1373 OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel); 1374 1375 if (isPrecedenceNode((Node) osm, p, useSelected)) return osm; 1376 WaySegment ws; 1377 if (useSelected) { 1378 ws = getNearestWaySegment(p, predicate, useSelected, sel); 1379 } else { 1380 ws = getNearestWaySegment(p, predicate, useSelected); 1381 } 1382 if (ws == null) return osm; 1383 1384 if ((ws.way.isSelected() && useSelected) || osm == null) { 1385 // either (no _selected_ nearest node found, if desired) or no nearest node was found 1386 osm = ws.way; 1387 } else { 1388 int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get(); 1389 maxWaySegLenSq *= maxWaySegLenSq; 1390 1391 Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex)); 1392 Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1)); 1393 1394 // is wayseg shorter than maxWaySegLenSq and 1395 // is p closer to the middle of wayseg than to the nearest node? 1396 if (wp1.distanceSq(wp2) < maxWaySegLenSq && 1397 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) { 1398 osm = ws.way; 1399 } 1400 } 1401 return osm; 1402 } 1403 1404 public static double perDist(Point2D pt, Point2D a, Point2D b) { 1405 if (pt != null && a != null && b != null) { 1406 double pd = 1407 (a.getX()-pt.getX())*(b.getX()-a.getX()) - 1408 (a.getY()-pt.getY())*(b.getY()-a.getY()); 1409 return Math.abs(pd) / a.distance(b); 1410 } 1411 return 0d; 1412 } 1413 1414 /** 1415 * 1416 * @param pt point to project onto (ab) 1417 * @param a root of vector 1418 * @param b vector 1419 * @return point of intersection of line given by (ab) 1420 * with its orthogonal line running through pt 1421 */ 1422 public static Point2D project(Point2D pt, Point2D a, Point2D b) { 1423 if (pt != null && a != null && b != null) { 1424 double r = ( 1425 (pt.getX()-a.getX())*(b.getX()-a.getX()) + 1426 (pt.getY()-a.getY())*(b.getY()-a.getY())) 1427 / a.distanceSq(b); 1428 return project(r, a, b); 1429 } 1430 return null; 1431 } 1432 1433 /** 1434 * if r = 0 returns a, if r=1 returns b, 1435 * if r = 0.5 returns center between a and b, etc.. 1436 * 1437 * @param r scale value 1438 * @param a root of vector 1439 * @param b vector 1440 * @return new point at a + r*(ab) 1441 */ 1442 public static Point2D project(double r, Point2D a, Point2D b) { 1443 Point2D ret = null; 1444 1445 if (a != null && b != null) { 1446 ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()), 1447 a.getY() + r*(b.getY()-a.getY())); 1448 } 1449 return ret; 1450 } 1451 1452 /** 1453 * The *result* does not depend on the current map selection state, neither does the result *order*. 1454 * It solely depends on the distance to point p. 1455 * 1456 * @param p The point on screen. 1457 * @param ignore a collection of ways which are not to be returned. 1458 * @param predicate the returned object has to fulfill certain properties. 1459 * 1460 * @return a list of all objects that are nearest to point p and 1461 * not in ignore or an empty list if nothing was found. 1462 */ 1463 public final List<OsmPrimitive> getAllNearest(Point p, 1464 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) { 1465 List<OsmPrimitive> nearestList = new ArrayList<>(); 1466 Set<Way> wset = new HashSet<>(); 1467 1468 // add nearby ways 1469 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { 1470 for (WaySegment ws : wss) { 1471 if (wset.add(ws.way)) { 1472 nearestList.add(ws.way); 1473 } 1474 } 1475 } 1476 1477 // add nearby nodes 1478 for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) { 1479 nearestList.addAll(nlist); 1480 } 1481 1482 // add parent relations of nearby nodes and ways 1483 Set<OsmPrimitive> parentRelations = new HashSet<>(); 1484 for (OsmPrimitive o : nearestList) { 1485 for (OsmPrimitive r : o.getReferrers()) { 1486 if (r instanceof Relation && predicate.evaluate(r)) { 1487 parentRelations.add(r); 1488 } 1489 } 1490 } 1491 nearestList.addAll(parentRelations); 1492 1493 if (ignore != null) { 1494 nearestList.removeAll(ignore); 1495 } 1496 1497 return nearestList; 1498 } 1499 1500 /** 1501 * The *result* does not depend on the current map selection state, neither does the result *order*. 1502 * It solely depends on the distance to point p. 1503 * 1504 * @param p The point on screen. 1505 * @param predicate the returned object has to fulfill certain properties. 1506 * 1507 * @return a list of all objects that are nearest to point p 1508 * or an empty list if nothing was found. 1509 * @see #getAllNearest(Point, Collection, Predicate) 1510 */ 1511 public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) { 1512 return getAllNearest(p, null, predicate); 1513 } 1514 1515 /** 1516 * @return The projection to be used in calculating stuff. 1517 */ 1518 public Projection getProjection() { 1519 return Main.getProjection(); 1520 } 1521 1522 @Override 1523 public String helpTopic() { 1524 String n = getClass().getName(); 1525 return n.substring(n.lastIndexOf('.')+1); 1526 } 1527 1528 /** 1529 * Return a ID which is unique as long as viewport dimensions are the same 1530 * @return A unique ID, as long as viewport dimensions are the same 1531 */ 1532 public int getViewID() { 1533 String x = center.east() + '_' + center.north() + '_' + scale + '_' + 1534 getWidth() + '_' + getHeight() + '_' + getProjection().toString(); 1535 CRC32 id = new CRC32(); 1536 id.update(x.getBytes(StandardCharsets.UTF_8)); 1537 return (int) id.getValue(); 1538 } 1539 1540 /** 1541 * Set new cursor. 1542 * @param cursor The new cursor to use. 1543 * @param reference A reference object that can be passed to the next set/reset calls to identify the caller. 1544 */ 1545 public void setNewCursor(Cursor cursor, Object reference) { 1546 cursorManager.setNewCursor(cursor, reference); 1547 } 1548 1549 /** 1550 * Set new cursor. 1551 * @param cursor the type of predefined cursor 1552 * @param reference A reference object that can be passed to the next set/reset calls to identify the caller. 1553 */ 1554 public void setNewCursor(int cursor, Object reference) { 1555 setNewCursor(Cursor.getPredefinedCursor(cursor), reference); 1556 } 1557 1558 /** 1559 * Remove the new cursor and reset to previous 1560 * @param reference Cursor reference 1561 */ 1562 public void resetCursor(Object reference) { 1563 cursorManager.resetCursor(reference); 1564 } 1565 1566 /** 1567 * Gets the cursor manager that is used for this NavigatableComponent. 1568 * @return The cursor manager. 1569 */ 1570 public CursorManager getCursorManager() { 1571 return cursorManager; 1572 } 1573 1574 @Override 1575 public void paint(Graphics g) { 1576 synchronized (paintRequestLock) { 1577 if (paintRect != null) { 1578 Graphics g2 = g.create(); 1579 g2.setColor(Utils.complement(PaintColors.getBackgroundColor())); 1580 g2.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height); 1581 g2.dispose(); 1582 } 1583 if (paintPoly != null) { 1584 Graphics g2 = g.create(); 1585 g2.setColor(Utils.complement(PaintColors.getBackgroundColor())); 1586 g2.drawPolyline(paintPoly.xpoints, paintPoly.ypoints, paintPoly.npoints); 1587 g2.dispose(); 1588 } 1589 } 1590 super.paint(g); 1591 } 1592 1593 /** 1594 * Requests to paint the given {@code Rectangle}. 1595 * @param r The Rectangle to draw 1596 * @see #requestClearRect 1597 * @since 5500 1598 */ 1599 public void requestPaintRect(Rectangle r) { 1600 if (r != null) { 1601 synchronized (paintRequestLock) { 1602 paintRect = r; 1603 } 1604 repaint(); 1605 } 1606 } 1607 1608 /** 1609 * Requests to paint the given {@code Polygon} as a polyline (unclosed polygon). 1610 * @param p The Polygon to draw 1611 * @see #requestClearPoly 1612 * @since 5500 1613 */ 1614 public void requestPaintPoly(Polygon p) { 1615 if (p != null) { 1616 synchronized (paintRequestLock) { 1617 paintPoly = p; 1618 } 1619 repaint(); 1620 } 1621 } 1622 1623 /** 1624 * Requests to clear the rectangled previously drawn. 1625 * @see #requestPaintRect 1626 * @since 5500 1627 */ 1628 public void requestClearRect() { 1629 synchronized (paintRequestLock) { 1630 paintRect = null; 1631 } 1632 repaint(); 1633 } 1634 1635 /** 1636 * Requests to clear the polyline previously drawn. 1637 * @see #requestPaintPoly 1638 * @since 5500 1639 */ 1640 public void requestClearPoly() { 1641 synchronized (paintRequestLock) { 1642 paintPoly = null; 1643 } 1644 repaint(); 1645 } 1646 1647 /** 1648 * Get a max scale for projection that describes world in 1/512 of the projection unit 1649 * @return max scale 1650 */ 1651 public double getMaxScale() { 1652 ProjectionBounds world = getMaxProjectionBounds(); 1653 return Math.max( 1654 world.maxNorth-world.minNorth, 1655 world.maxEast-world.minEast 1656 )/512; 1657 } 1658}