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