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