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