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