001 // License: GPL. See LICENSE file for details. 002 package org.openstreetmap.josm.gui; 003 004 import static org.openstreetmap.josm.tools.I18n.marktr; 005 006 import java.awt.Cursor; 007 import java.awt.Point; 008 import java.awt.Rectangle; 009 import java.awt.geom.AffineTransform; 010 import java.awt.geom.Point2D; 011 import java.util.ArrayList; 012 import java.util.Collection; 013 import java.util.Collections; 014 import java.util.Date; 015 import java.util.HashSet; 016 import java.util.LinkedHashMap; 017 import java.util.LinkedList; 018 import java.util.List; 019 import java.util.Locale; 020 import java.util.Map; 021 import java.util.Set; 022 import java.util.Stack; 023 import java.util.TreeMap; 024 import java.util.concurrent.CopyOnWriteArrayList; 025 026 import javax.swing.JComponent; 027 028 import org.openstreetmap.josm.Main; 029 import org.openstreetmap.josm.data.Bounds; 030 import org.openstreetmap.josm.data.ProjectionBounds; 031 import org.openstreetmap.josm.data.coor.CachedLatLon; 032 import org.openstreetmap.josm.data.coor.EastNorth; 033 import org.openstreetmap.josm.data.coor.LatLon; 034 import org.openstreetmap.josm.data.osm.BBox; 035 import org.openstreetmap.josm.data.osm.DataSet; 036 import org.openstreetmap.josm.data.osm.Node; 037 import org.openstreetmap.josm.data.osm.OsmPrimitive; 038 import org.openstreetmap.josm.data.osm.Relation; 039 import org.openstreetmap.josm.data.osm.Way; 040 import org.openstreetmap.josm.data.osm.WaySegment; 041 import org.openstreetmap.josm.data.preferences.IntegerProperty; 042 import org.openstreetmap.josm.data.projection.Projection; 043 import org.openstreetmap.josm.data.projection.Projections; 044 import org.openstreetmap.josm.gui.help.Helpful; 045 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 046 import org.openstreetmap.josm.tools.Predicate; 047 048 /** 049 * An component that can be navigated by a mapmover. Used as map view and for the 050 * zoomer in the download dialog. 051 * 052 * @author imi 053 */ 054 public class NavigatableComponent extends JComponent implements Helpful { 055 056 /** 057 * Interface to notify listeners of the change of the zoom area. 058 */ 059 public interface ZoomChangeListener { 060 void zoomChanged(); 061 } 062 063 public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10); 064 065 public static final String PROPNAME_CENTER = "center"; 066 public static final String PROPNAME_SCALE = "scale"; 067 068 /** 069 * the zoom listeners 070 */ 071 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>(); 072 073 /** 074 * Removes a zoom change listener 075 * 076 * @param listener the listener. Ignored if null or already absent 077 */ 078 public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) { 079 zoomChangeListeners.remove(listener); 080 } 081 082 /** 083 * Adds a zoom change listener 084 * 085 * @param listener the listener. Ignored if null or already registered. 086 */ 087 public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) { 088 if (listener != null) { 089 zoomChangeListeners.addIfAbsent(listener); 090 } 091 } 092 093 protected static void fireZoomChanged() { 094 for (ZoomChangeListener l : zoomChangeListeners) { 095 l.zoomChanged(); 096 } 097 } 098 099 /** 100 * The scale factor in x or y-units per pixel. This means, if scale = 10, 101 * every physical pixel on screen are 10 x or 10 y units in the 102 * northing/easting space of the projection. 103 */ 104 private double scale = Main.getProjection().getDefaultZoomInPPD(); 105 /** 106 * Center n/e coordinate of the desired screen center. 107 */ 108 protected EastNorth center = calculateDefaultCenter(); 109 110 public NavigatableComponent() { 111 setLayout(null); 112 } 113 114 protected DataSet getCurrentDataSet() { 115 return Main.main.getCurrentDataSet(); 116 } 117 118 private EastNorth calculateDefaultCenter() { 119 Bounds b = Main.getProjection().getWorldBoundsLatLon(); 120 double lat = (b.getMax().lat() + b.getMin().lat())/2; 121 double lon = (b.getMax().lon() + b.getMin().lon())/2; 122 123 return Main.getProjection().latlon2eastNorth(new LatLon(lat, lon)); 124 } 125 126 public static String getDistText(double dist) { 127 return getSystemOfMeasurement().getDistText(dist); 128 } 129 130 public String getDist100PixelText() 131 { 132 return getDistText(getDist100Pixel()); 133 } 134 135 public double getDist100Pixel() 136 { 137 int w = getWidth()/2; 138 int h = getHeight()/2; 139 LatLon ll1 = getLatLon(w-50,h); 140 LatLon ll2 = getLatLon(w+50,h); 141 return ll1.greatCircleDistance(ll2); 142 } 143 144 /** 145 * @return Returns the center point. A copy is returned, so users cannot 146 * change the center by accessing the return value. Use zoomTo instead. 147 */ 148 public EastNorth getCenter() { 149 return center; 150 } 151 152 /** 153 * @param x X-Pixelposition to get coordinate from 154 * @param y Y-Pixelposition to get coordinate from 155 * 156 * @return Geographic coordinates from a specific pixel coordination 157 * on the screen. 158 */ 159 public EastNorth getEastNorth(int x, int y) { 160 return new EastNorth( 161 center.east() + (x - getWidth()/2.0)*scale, 162 center.north() - (y - getHeight()/2.0)*scale); 163 } 164 165 public ProjectionBounds getProjectionBounds() { 166 return new ProjectionBounds( 167 new EastNorth( 168 center.east() - getWidth()/2.0*scale, 169 center.north() - getHeight()/2.0*scale), 170 new EastNorth( 171 center.east() + getWidth()/2.0*scale, 172 center.north() + getHeight()/2.0*scale)); 173 } 174 175 /* FIXME: replace with better method - used by MapSlider */ 176 public ProjectionBounds getMaxProjectionBounds() { 177 Bounds b = getProjection().getWorldBoundsLatLon(); 178 return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()), 179 getProjection().latlon2eastNorth(b.getMax())); 180 } 181 182 /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */ 183 public Bounds getRealBounds() { 184 return new Bounds( 185 getProjection().eastNorth2latlon(new EastNorth( 186 center.east() - getWidth()/2.0*scale, 187 center.north() - getHeight()/2.0*scale)), 188 getProjection().eastNorth2latlon(new EastNorth( 189 center.east() + getWidth()/2.0*scale, 190 center.north() + getHeight()/2.0*scale))); 191 } 192 193 /** 194 * @param x X-Pixelposition to get coordinate from 195 * @param y Y-Pixelposition to get coordinate from 196 * 197 * @return Geographic unprojected coordinates from a specific pixel coordination 198 * on the screen. 199 */ 200 public LatLon getLatLon(int x, int y) { 201 return getProjection().eastNorth2latlon(getEastNorth(x, y)); 202 } 203 204 public LatLon getLatLon(double x, double y) { 205 return getLatLon((int)x, (int)y); 206 } 207 208 /** 209 * @param r 210 * @return Minimum bounds that will cover rectangle 211 */ 212 public Bounds getLatLonBounds(Rectangle r) { 213 // TODO Maybe this should be (optional) method of Projection implementation 214 EastNorth p1 = getEastNorth(r.x, r.y); 215 EastNorth p2 = getEastNorth(r.x + r.width, r.y + r.height); 216 217 Bounds result = new Bounds(Main.getProjection().eastNorth2latlon(p1)); 218 219 double eastMin = Math.min(p1.east(), p2.east()); 220 double eastMax = Math.max(p1.east(), p2.east()); 221 double northMin = Math.min(p1.north(), p2.north()); 222 double northMax = Math.max(p1.north(), p2.north()); 223 double deltaEast = (eastMax - eastMin) / 10; 224 double deltaNorth = (northMax - northMin) / 10; 225 226 for (int i=0; i < 10; i++) { 227 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin))); 228 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax))); 229 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth))); 230 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth))); 231 } 232 233 return result; 234 } 235 236 public AffineTransform getAffineTransform() { 237 return new AffineTransform( 238 1.0/scale, 0.0, 0.0, -1.0/scale, getWidth()/2.0 - center.east()/scale, getHeight()/2.0 + center.north()/scale); 239 } 240 241 /** 242 * Return the point on the screen where this Coordinate would be. 243 * @param p The point, where this geopoint would be drawn. 244 * @return The point on screen where "point" would be drawn, relative 245 * to the own top/left. 246 */ 247 public Point2D getPoint2D(EastNorth p) { 248 if (null == p) 249 return new Point(); 250 double x = (p.east()-center.east())/scale + getWidth()/2; 251 double y = (center.north()-p.north())/scale + getHeight()/2; 252 return new Point2D.Double(x, y); 253 } 254 255 public Point2D getPoint2D(LatLon latlon) { 256 if (latlon == null) 257 return new Point(); 258 else if (latlon instanceof CachedLatLon) 259 return getPoint2D(((CachedLatLon)latlon).getEastNorth()); 260 else 261 return getPoint2D(getProjection().latlon2eastNorth(latlon)); 262 } 263 264 public Point2D getPoint2D(Node n) { 265 return getPoint2D(n.getEastNorth()); 266 } 267 268 // looses precision, may overflow (depends on p and current scale) 269 //@Deprecated 270 public Point getPoint(EastNorth p) { 271 Point2D d = getPoint2D(p); 272 return new Point((int) d.getX(), (int) d.getY()); 273 } 274 275 // looses precision, may overflow (depends on p and current scale) 276 //@Deprecated 277 public Point getPoint(LatLon latlon) { 278 Point2D d = getPoint2D(latlon); 279 return new Point((int) d.getX(), (int) d.getY()); 280 } 281 282 // looses precision, may overflow (depends on p and current scale) 283 //@Deprecated 284 public Point getPoint(Node n) { 285 Point2D d = getPoint2D(n); 286 return new Point((int) d.getX(), (int) d.getY()); 287 } 288 289 /** 290 * Zoom to the given coordinate. 291 * @param newCenter The center x-value (easting) to zoom to. 292 * @param scale The scale to use. 293 */ 294 public void zoomTo(EastNorth newCenter, double newScale) { 295 Bounds b = getProjection().getWorldBoundsLatLon(); 296 LatLon cl = Projections.inverseProject(newCenter); 297 boolean changed = false; 298 double lat = cl.lat(); 299 double lon = cl.lon(); 300 if(lat < b.getMin().lat()) {changed = true; lat = b.getMin().lat(); } 301 else if(lat > b.getMax().lat()) {changed = true; lat = b.getMax().lat(); } 302 if(lon < b.getMin().lon()) {changed = true; lon = b.getMin().lon(); } 303 else if(lon > b.getMax().lon()) {changed = true; lon = b.getMax().lon(); } 304 if(changed) { 305 newCenter = Projections.project(new LatLon(lat,lon)); 306 } 307 int width = getWidth()/2; 308 int height = getHeight()/2; 309 LatLon l1 = new LatLon(b.getMin().lat(), lon); 310 LatLon l2 = new LatLon(b.getMax().lat(), lon); 311 EastNorth e1 = getProjection().latlon2eastNorth(l1); 312 EastNorth e2 = getProjection().latlon2eastNorth(l2); 313 double d = e2.north() - e1.north(); 314 if(d < height*newScale) 315 { 316 double newScaleH = d/height; 317 e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMin().lon())); 318 e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMax().lon())); 319 d = e2.east() - e1.east(); 320 if(d < width*newScale) { 321 newScale = Math.max(newScaleH, d/width); 322 } 323 } 324 else 325 { 326 d = d/(l1.greatCircleDistance(l2)*height*10); 327 if(newScale < d) { 328 newScale = d; 329 } 330 } 331 332 if (!newCenter.equals(center) || (scale != newScale)) { 333 pushZoomUndo(center, scale); 334 zoomNoUndoTo(newCenter, newScale); 335 } 336 } 337 338 /** 339 * Zoom to the given coordinate without adding to the zoom undo buffer. 340 * @param newCenter The center x-value (easting) to zoom to. 341 * @param scale The scale to use. 342 */ 343 private void zoomNoUndoTo(EastNorth newCenter, double newScale) { 344 if (!newCenter.equals(center)) { 345 EastNorth oldCenter = center; 346 center = newCenter; 347 firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter); 348 } 349 if (scale != newScale) { 350 double oldScale = scale; 351 scale = newScale; 352 firePropertyChange(PROPNAME_SCALE, oldScale, newScale); 353 } 354 355 repaint(); 356 fireZoomChanged(); 357 } 358 359 public void zoomTo(EastNorth newCenter) { 360 zoomTo(newCenter, scale); 361 } 362 363 public void zoomTo(LatLon newCenter) { 364 zoomTo(Projections.project(newCenter)); 365 } 366 367 public void smoothScrollTo(LatLon newCenter) { 368 smoothScrollTo(Projections.project(newCenter)); 369 } 370 371 /** 372 * Create a thread that moves the viewport to the given center in an 373 * animated fashion. 374 */ 375 public void smoothScrollTo(EastNorth newCenter) { 376 // fixme make these configurable. 377 final int fps = 20; // animation frames per second 378 final int speed = 1500; // milliseconds for full-screen-width pan 379 if (!newCenter.equals(center)) { 380 final EastNorth oldCenter = center; 381 final double distance = newCenter.distance(oldCenter) / scale; 382 final double milliseconds = distance / getWidth() * speed; 383 final double frames = milliseconds * fps / 1000; 384 final EastNorth finalNewCenter = newCenter; 385 386 new Thread(){ 387 @Override 388 public void run() { 389 for (int i=0; i<frames; i++) 390 { 391 // fixme - not use zoom history here 392 zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames)); 393 try { Thread.sleep(1000 / fps); } catch (InterruptedException ex) { }; 394 } 395 } 396 }.start(); 397 } 398 } 399 400 public void zoomToFactor(double x, double y, double factor) { 401 double newScale = scale*factor; 402 // New center position so that point under the mouse pointer stays the same place as it was before zooming 403 // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom 404 zoomTo(new EastNorth( 405 center.east() - (x - getWidth()/2.0) * (newScale - scale), 406 center.north() + (y - getHeight()/2.0) * (newScale - scale)), 407 newScale); 408 } 409 410 public void zoomToFactor(EastNorth newCenter, double factor) { 411 zoomTo(newCenter, scale*factor); 412 } 413 414 public void zoomToFactor(double factor) { 415 zoomTo(center, scale*factor); 416 } 417 418 public void zoomTo(ProjectionBounds box) { 419 // -20 to leave some border 420 int w = getWidth()-20; 421 if (w < 20) { 422 w = 20; 423 } 424 int h = getHeight()-20; 425 if (h < 20) { 426 h = 20; 427 } 428 429 double scaleX = (box.maxEast-box.minEast)/w; 430 double scaleY = (box.maxNorth-box.minNorth)/h; 431 double newScale = Math.max(scaleX, scaleY); 432 433 zoomTo(box.getCenter(), newScale); 434 } 435 436 public void zoomTo(Bounds box) { 437 zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()), 438 getProjection().latlon2eastNorth(box.getMax()))); 439 } 440 441 private class ZoomData { 442 LatLon center; 443 double scale; 444 445 public ZoomData(EastNorth center, double scale) { 446 this.center = Projections.inverseProject(center); 447 this.scale = scale; 448 } 449 450 public EastNorth getCenterEastNorth() { 451 return getProjection().latlon2eastNorth(center); 452 } 453 454 public double getScale() { 455 return scale; 456 } 457 } 458 459 private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>(); 460 private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>(); 461 private Date zoomTimestamp = new Date(); 462 463 private void pushZoomUndo(EastNorth center, double scale) { 464 Date now = new Date(); 465 if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) { 466 zoomUndoBuffer.push(new ZoomData(center, scale)); 467 if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) { 468 zoomUndoBuffer.remove(0); 469 } 470 zoomRedoBuffer.clear(); 471 } 472 zoomTimestamp = now; 473 } 474 475 public void zoomPrevious() { 476 if (!zoomUndoBuffer.isEmpty()) { 477 ZoomData zoom = zoomUndoBuffer.pop(); 478 zoomRedoBuffer.push(new ZoomData(center, scale)); 479 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale()); 480 } 481 } 482 483 public void zoomNext() { 484 if (!zoomRedoBuffer.isEmpty()) { 485 ZoomData zoom = zoomRedoBuffer.pop(); 486 zoomUndoBuffer.push(new ZoomData(center, scale)); 487 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale()); 488 } 489 } 490 491 public boolean hasZoomUndoEntries() { 492 return !zoomUndoBuffer.isEmpty(); 493 } 494 495 public boolean hasZoomRedoEntries() { 496 return !zoomRedoBuffer.isEmpty(); 497 } 498 499 private BBox getBBox(Point p, int snapDistance) { 500 return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance), 501 getLatLon(p.x + snapDistance, p.y + snapDistance)); 502 } 503 504 /** 505 * The *result* does not depend on the current map selection state, 506 * neither does the result *order*. 507 * It solely depends on the distance to point p. 508 * 509 * @return a sorted map with the keys representing the distance of 510 * their associated nodes to point p. 511 */ 512 private Map<Double, List<Node>> getNearestNodesImpl(Point p, 513 Predicate<OsmPrimitive> predicate) { 514 TreeMap<Double, List<Node>> nearestMap = new TreeMap<Double, List<Node>>(); 515 DataSet ds = getCurrentDataSet(); 516 517 if (ds != null) { 518 double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get(); 519 snapDistanceSq *= snapDistanceSq; 520 521 for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) { 522 if (predicate.evaluate(n) 523 && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) 524 { 525 List<Node> nlist; 526 if (nearestMap.containsKey(dist)) { 527 nlist = nearestMap.get(dist); 528 } else { 529 nlist = new LinkedList<Node>(); 530 nearestMap.put(dist, nlist); 531 } 532 nlist.add(n); 533 } 534 } 535 } 536 537 return nearestMap; 538 } 539 540 /** 541 * The *result* does not depend on the current map selection state, 542 * neither does the result *order*. 543 * It solely depends on the distance to point p. 544 * 545 * @return All nodes nearest to point p that are in a belt from 546 * dist(nearest) to dist(nearest)+4px around p and 547 * that are not in ignore. 548 * 549 * @param p the point for which to search the nearest segment. 550 * @param ignore a collection of nodes which are not to be returned. 551 * @param predicate the returned objects have to fulfill certain properties. 552 */ 553 public final List<Node> getNearestNodes(Point p, 554 Collection<Node> ignore, Predicate<OsmPrimitive> predicate) { 555 List<Node> nearestList = Collections.emptyList(); 556 557 if (ignore == null) { 558 ignore = Collections.emptySet(); 559 } 560 561 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate); 562 if (!nlists.isEmpty()) { 563 Double minDistSq = null; 564 List<Node> nlist; 565 for (Double distSq : nlists.keySet()) { 566 nlist = nlists.get(distSq); 567 568 // filter nodes to be ignored before determining minDistSq.. 569 nlist.removeAll(ignore); 570 if (minDistSq == null) { 571 if (!nlist.isEmpty()) { 572 minDistSq = distSq; 573 nearestList = new ArrayList<Node>(); 574 nearestList.addAll(nlist); 575 } 576 } else { 577 if (distSq-minDistSq < (4)*(4)) { 578 nearestList.addAll(nlist); 579 } 580 } 581 } 582 } 583 584 return nearestList; 585 } 586 587 /** 588 * The *result* does not depend on the current map selection state, 589 * neither does the result *order*. 590 * It solely depends on the distance to point p. 591 * 592 * @return All nodes nearest to point p that are in a belt from 593 * dist(nearest) to dist(nearest)+4px around p. 594 * @see #getNearestNodes(Point, Collection, Predicate) 595 * 596 * @param p the point for which to search the nearest segment. 597 * @param predicate the returned objects have to fulfill certain properties. 598 */ 599 public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) { 600 return getNearestNodes(p, null, predicate); 601 } 602 603 /** 604 * The *result* depends on the current map selection state IF use_selected is true. 605 * 606 * If more than one node within node.snap-distance pixels is found, 607 * the nearest node selected is returned IF use_selected is true. 608 * 609 * Else the nearest new/id=0 node within about the same distance 610 * as the true nearest node is returned. 611 * 612 * If no such node is found either, the true nearest 613 * node to p is returned. 614 * 615 * Finally, if a node is not found at all, null is returned. 616 * 617 * @return A node within snap-distance to point p, 618 * that is chosen by the algorithm described. 619 * 620 * @param p the screen point 621 * @param predicate this parameter imposes a condition on the returned object, e.g. 622 * give the nearest node that is tagged. 623 */ 624 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) { 625 Node n = null; 626 627 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate); 628 if (!nlists.isEmpty()) { 629 Node ntsel = null, ntnew = null; 630 double minDistSq = nlists.keySet().iterator().next(); 631 632 for (Double distSq : nlists.keySet()) { 633 for (Node nd : nlists.get(distSq)) { 634 // find the nearest selected node 635 if (ntsel == null && nd.isSelected()) { 636 ntsel = nd; 637 // if there are multiple nearest nodes, prefer the one 638 // that is selected. This is required in order to drag 639 // the selected node if multiple nodes have the same 640 // coordinates (e.g. after unglue) 641 use_selected |= (distSq == minDistSq); 642 } 643 // find the nearest newest node that is within about the same 644 // distance as the true nearest node 645 if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) { 646 ntnew = nd; 647 } 648 } 649 } 650 651 // take nearest selected, nearest new or true nearest node to p, in that order 652 n = (ntsel != null && use_selected) ? ntsel 653 : (ntnew != null) ? ntnew 654 : nlists.values().iterator().next().get(0); 655 } 656 return n; 657 } 658 659 /** 660 * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}. 661 * 662 * @return The nearest node to point p. 663 */ 664 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) { 665 return getNearestNode(p, predicate, true); 666 } 667 668 /** 669 * The *result* does not depend on the current map selection state, 670 * neither does the result *order*. 671 * It solely depends on the distance to point p. 672 * 673 * @return a sorted map with the keys representing the perpendicular 674 * distance of their associated way segments to point p. 675 */ 676 private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, 677 Predicate<OsmPrimitive> predicate) { 678 Map<Double, List<WaySegment>> nearestMap = new TreeMap<Double, List<WaySegment>>(); 679 DataSet ds = getCurrentDataSet(); 680 681 if (ds != null) { 682 double snapDistanceSq = Main.pref.getInteger("mappaint.segment.snap-distance", 10); 683 snapDistanceSq *= snapDistanceSq; 684 685 for (Way w : ds.searchWays(getBBox(p, Main.pref.getInteger("mappaint.segment.snap-distance", 10)))) { 686 if (!predicate.evaluate(w)) { 687 continue; 688 } 689 Node lastN = null; 690 int i = -2; 691 for (Node n : w.getNodes()) { 692 i++; 693 if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception? 694 continue; 695 } 696 if (lastN == null) { 697 lastN = n; 698 continue; 699 } 700 701 Point2D A = getPoint2D(lastN); 702 Point2D B = getPoint2D(n); 703 double c = A.distanceSq(B); 704 double a = p.distanceSq(B); 705 double b = p.distanceSq(A); 706 707 /* perpendicular distance squared 708 * loose some precision to account for possible deviations in the calculation above 709 * e.g. if identical (A and B) come about reversed in another way, values may differ 710 * -- zero out least significant 32 dual digits of mantissa.. 711 */ 712 double perDistSq = Double.longBitsToDouble( 713 Double.doubleToLongBits( a - (a - b + c) * (a - b + c) / 4 / c ) 714 >> 32 << 32); // resolution in numbers with large exponent not needed here.. 715 716 if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) { 717 //System.err.println(Double.toHexString(perDistSq)); 718 719 List<WaySegment> wslist; 720 if (nearestMap.containsKey(perDistSq)) { 721 wslist = nearestMap.get(perDistSq); 722 } else { 723 wslist = new LinkedList<WaySegment>(); 724 nearestMap.put(perDistSq, wslist); 725 } 726 wslist.add(new WaySegment(w, i)); 727 } 728 729 lastN = n; 730 } 731 } 732 } 733 734 return nearestMap; 735 } 736 737 /** 738 * The result *order* depends on the current map selection state. 739 * Segments within 10px of p are searched and sorted by their distance to @param p, 740 * then, within groups of equally distant segments, prefer those that are selected. 741 * 742 * @return all segments within 10px of p that are not in ignore, 743 * sorted by their perpendicular distance. 744 * 745 * @param p the point for which to search the nearest segments. 746 * @param ignore a collection of segments which are not to be returned. 747 * @param predicate the returned objects have to fulfill certain properties. 748 */ 749 public final List<WaySegment> getNearestWaySegments(Point p, 750 Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) { 751 List<WaySegment> nearestList = new ArrayList<WaySegment>(); 752 List<WaySegment> unselected = new LinkedList<WaySegment>(); 753 754 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { 755 // put selected waysegs within each distance group first 756 // makes the order of nearestList dependent on current selection state 757 for (WaySegment ws : wss) { 758 (ws.way.isSelected() ? nearestList : unselected).add(ws); 759 } 760 nearestList.addAll(unselected); 761 unselected.clear(); 762 } 763 if (ignore != null) { 764 nearestList.removeAll(ignore); 765 } 766 767 return nearestList; 768 } 769 770 /** 771 * The result *order* depends on the current map selection state. 772 * 773 * @return all segments within 10px of p, sorted by their perpendicular distance. 774 * @see #getNearestWaySegments(Point, Collection, Predicate) 775 * 776 * @param p the point for which to search the nearest segments. 777 * @param predicate the returned objects have to fulfill certain properties. 778 */ 779 public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) { 780 return getNearestWaySegments(p, null, predicate); 781 } 782 783 /** 784 * The *result* depends on the current map selection state IF use_selected is true. 785 * 786 * @return The nearest way segment to point p, 787 * and, depending on use_selected, prefers a selected way segment, if found. 788 * @see #getNearestWaySegments(Point, Collection, Predicate) 789 * 790 * @param p the point for which to search the nearest segment. 791 * @param predicate the returned object has to fulfill certain properties. 792 * @param use_selected whether selected way segments should be preferred. 793 */ 794 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) { 795 WaySegment wayseg = null, ntsel = null; 796 797 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) { 798 if (wayseg != null && ntsel != null) { 799 break; 800 } 801 for (WaySegment ws : wslist) { 802 if (wayseg == null) { 803 wayseg = ws; 804 } 805 if (ntsel == null && ws.way.isSelected()) { 806 ntsel = ws; 807 } 808 } 809 } 810 811 return (ntsel != null && use_selected) ? ntsel : wayseg; 812 } 813 814 /** 815 * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}. 816 * 817 * @return The nearest way segment to point p. 818 */ 819 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) { 820 return getNearestWaySegment(p, predicate, true); 821 } 822 823 /** 824 * The *result* does not depend on the current map selection state, 825 * neither does the result *order*. 826 * It solely depends on the perpendicular distance to point p. 827 * 828 * @return all nearest ways to the screen point given that are not in ignore. 829 * @see #getNearestWaySegments(Point, Collection, Predicate) 830 * 831 * @param p the point for which to search the nearest ways. 832 * @param ignore a collection of ways which are not to be returned. 833 * @param predicate the returned object has to fulfill certain properties. 834 */ 835 public final List<Way> getNearestWays(Point p, 836 Collection<Way> ignore, Predicate<OsmPrimitive> predicate) { 837 List<Way> nearestList = new ArrayList<Way>(); 838 Set<Way> wset = new HashSet<Way>(); 839 840 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { 841 for (WaySegment ws : wss) { 842 if (wset.add(ws.way)) { 843 nearestList.add(ws.way); 844 } 845 } 846 } 847 if (ignore != null) { 848 nearestList.removeAll(ignore); 849 } 850 851 return nearestList; 852 } 853 854 /** 855 * The *result* does not depend on the current map selection state, 856 * neither does the result *order*. 857 * It solely depends on the perpendicular distance to point p. 858 * 859 * @return all nearest ways to the screen point given. 860 * @see #getNearestWays(Point, Collection, Predicate) 861 * 862 * @param p the point for which to search the nearest ways. 863 * @param predicate the returned object has to fulfill certain properties. 864 */ 865 public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) { 866 return getNearestWays(p, null, predicate); 867 } 868 869 /** 870 * The *result* depends on the current map selection state. 871 * 872 * @return The nearest way to point p, 873 * prefer a selected way if there are multiple nearest. 874 * @see #getNearestWaySegment(Point, Collection, Predicate) 875 * 876 * @param p the point for which to search the nearest segment. 877 * @param predicate the returned object has to fulfill certain properties. 878 */ 879 public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) { 880 WaySegment nearestWaySeg = getNearestWaySegment(p, predicate); 881 return (nearestWaySeg == null) ? null : nearestWaySeg.way; 882 } 883 884 /** 885 * The *result* does not depend on the current map selection state, 886 * neither does the result *order*. 887 * It solely depends on the distance to point p. 888 * 889 * First, nodes will be searched. If there are nodes within BBox found, 890 * return a collection of those nodes only. 891 * 892 * If no nodes are found, search for nearest ways. If there are ways 893 * within BBox found, return a collection of those ways only. 894 * 895 * If nothing is found, return an empty collection. 896 * 897 * @return Primitives nearest to the given screen point that are not in ignore. 898 * @see #getNearestNodes(Point, Collection, Predicate) 899 * @see #getNearestWays(Point, Collection, Predicate) 900 * 901 * @param p The point on screen. 902 * @param ignore a collection of ways which are not to be returned. 903 * @param predicate the returned object has to fulfill certain properties. 904 */ 905 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, 906 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) { 907 List<OsmPrimitive> nearestList = Collections.emptyList(); 908 OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false); 909 910 if (osm != null) { 911 if (osm instanceof Node) { 912 nearestList = new ArrayList<OsmPrimitive>(getNearestNodes(p, predicate)); 913 } else if (osm instanceof Way) { 914 nearestList = new ArrayList<OsmPrimitive>(getNearestWays(p, predicate)); 915 } 916 if (ignore != null) { 917 nearestList.removeAll(ignore); 918 } 919 } 920 921 return nearestList; 922 } 923 924 /** 925 * The *result* does not depend on the current map selection state, 926 * neither does the result *order*. 927 * It solely depends on the distance to point p. 928 * 929 * @return Primitives nearest to the given screen point. 930 * @see #getNearests(Point, Collection, Predicate) 931 * 932 * @param p The point on screen. 933 * @param predicate the returned object has to fulfill certain properties. 934 */ 935 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) { 936 return getNearestNodesOrWays(p, null, predicate); 937 } 938 939 /** 940 * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)} 941 * It decides, whether to yield the node to be tested or look for further (way) candidates. 942 * 943 * @return true, if the node fulfills the properties of the function body 944 * 945 * @param osm node to check 946 * @param p point clicked 947 * @param use_selected whether to prefer selected nodes 948 */ 949 private boolean isPrecedenceNode(Node osm, Point p, boolean use_selected) { 950 boolean ret = false; 951 952 if (osm != null) { 953 ret |= !(p.distanceSq(getPoint2D(osm)) > (4)*(4)); 954 ret |= osm.isTagged(); 955 if (use_selected) { 956 ret |= osm.isSelected(); 957 } 958 } 959 960 return ret; 961 } 962 963 /** 964 * The *result* depends on the current map selection state IF use_selected is true. 965 * 966 * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find 967 * the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)} 968 * to find the nearest selected way. 969 * 970 * IF use_selected is false, or if no selected primitive was found, do the following. 971 * 972 * If the nearest node found is within 4px of p, simply take it. 973 * Else, find the nearest way segment. Then, if p is closer to its 974 * middle than to the node, take the way segment, else take the node. 975 * 976 * Finally, if no nearest primitive is found at all, return null. 977 * 978 * @return A primitive within snap-distance to point p, 979 * that is chosen by the algorithm described. 980 * @see getNearestNode(Point, Predicate) 981 * @see getNearestNodesImpl(Point, Predicate) 982 * @see getNearestWay(Point, Predicate) 983 * 984 * @param p The point on screen. 985 * @param predicate the returned object has to fulfill certain properties. 986 * @param use_selected whether to prefer primitives that are currently selected. 987 */ 988 public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) { 989 OsmPrimitive osm = getNearestNode(p, predicate, use_selected); 990 WaySegment ws = null; 991 992 if (!isPrecedenceNode((Node)osm, p, use_selected)) { 993 ws = getNearestWaySegment(p, predicate, use_selected); 994 995 if (ws != null) { 996 if ((ws.way.isSelected() && use_selected) || osm == null) { 997 // either (no _selected_ nearest node found, if desired) or no nearest node was found 998 osm = ws.way; 999 } else { 1000 int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get(); 1001 maxWaySegLenSq *= maxWaySegLenSq; 1002 1003 Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex)); 1004 Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1)); 1005 1006 // is wayseg shorter than maxWaySegLenSq and 1007 // is p closer to the middle of wayseg than to the nearest node? 1008 if (wp1.distanceSq(wp2) < maxWaySegLenSq && 1009 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node)osm))) { 1010 osm = ws.way; 1011 } 1012 } 1013 } 1014 } 1015 1016 return osm; 1017 } 1018 1019 /** 1020 * @return o as collection of o's type. 1021 */ 1022 public static <T> Collection<T> asColl(T o) { 1023 if (o == null) 1024 return Collections.emptySet(); 1025 return Collections.singleton(o); 1026 } 1027 1028 public static double perDist(Point2D pt, Point2D a, Point2D b) { 1029 if (pt != null && a != null && b != null) { 1030 double pd = ( 1031 (a.getX()-pt.getX())*(b.getX()-a.getX()) - 1032 (a.getY()-pt.getY())*(b.getY()-a.getY()) ); 1033 return Math.abs(pd) / a.distance(b); 1034 } 1035 return 0d; 1036 } 1037 1038 /** 1039 * 1040 * @param pt point to project onto (ab) 1041 * @param a root of vector 1042 * @param b vector 1043 * @return point of intersection of line given by (ab) 1044 * with its orthogonal line running through pt 1045 */ 1046 public static Point2D project(Point2D pt, Point2D a, Point2D b) { 1047 if (pt != null && a != null && b != null) { 1048 double r = (( 1049 (pt.getX()-a.getX())*(b.getX()-a.getX()) + 1050 (pt.getY()-a.getY())*(b.getY()-a.getY()) ) 1051 / a.distanceSq(b)); 1052 return project(r, a, b); 1053 } 1054 return null; 1055 } 1056 1057 /** 1058 * if r = 0 returns a, if r=1 returns b, 1059 * if r = 0.5 returns center between a and b, etc.. 1060 * 1061 * @param r scale value 1062 * @param a root of vector 1063 * @param b vector 1064 * @return new point at a + r*(ab) 1065 */ 1066 public static Point2D project(double r, Point2D a, Point2D b) { 1067 Point2D ret = null; 1068 1069 if (a != null && b != null) { 1070 ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()), 1071 a.getY() + r*(b.getY()-a.getY())); 1072 } 1073 return ret; 1074 } 1075 1076 /** 1077 * The *result* does not depend on the current map selection state, 1078 * neither does the result *order*. 1079 * It solely depends on the distance to point p. 1080 * 1081 * @return a list of all objects that are nearest to point p and 1082 * not in ignore or an empty list if nothing was found. 1083 * 1084 * @param p The point on screen. 1085 * @param ignore a collection of ways which are not to be returned. 1086 * @param predicate the returned object has to fulfill certain properties. 1087 */ 1088 public final List<OsmPrimitive> getAllNearest(Point p, 1089 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) { 1090 List<OsmPrimitive> nearestList = new ArrayList<OsmPrimitive>(); 1091 Set<Way> wset = new HashSet<Way>(); 1092 1093 // add nearby ways 1094 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { 1095 for (WaySegment ws : wss) { 1096 if (wset.add(ws.way)) { 1097 nearestList.add(ws.way); 1098 } 1099 } 1100 } 1101 1102 // add nearby nodes 1103 for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) { 1104 nearestList.addAll(nlist); 1105 } 1106 1107 // add parent relations of nearby nodes and ways 1108 Set<OsmPrimitive> parentRelations = new HashSet<OsmPrimitive>(); 1109 for (OsmPrimitive o : nearestList) { 1110 for (OsmPrimitive r : o.getReferrers()) { 1111 if (r instanceof Relation && predicate.evaluate(r)) { 1112 parentRelations.add(r); 1113 } 1114 } 1115 } 1116 nearestList.addAll(parentRelations); 1117 1118 if (ignore != null) { 1119 nearestList.removeAll(ignore); 1120 } 1121 1122 return nearestList; 1123 } 1124 1125 /** 1126 * The *result* does not depend on the current map selection state, 1127 * neither does the result *order*. 1128 * It solely depends on the distance to point p. 1129 * 1130 * @return a list of all objects that are nearest to point p 1131 * or an empty list if nothing was found. 1132 * @see #getAllNearest(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> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) { 1138 return getAllNearest(p, null, predicate); 1139 } 1140 1141 /** 1142 * @return The projection to be used in calculating stuff. 1143 */ 1144 public Projection getProjection() { 1145 return Main.getProjection(); 1146 } 1147 1148 public String helpTopic() { 1149 String n = getClass().getName(); 1150 return n.substring(n.lastIndexOf('.')+1); 1151 } 1152 1153 /** 1154 * Return a ID which is unique as long as viewport dimensions are the same 1155 */ 1156 public int getViewID() { 1157 String x = center.east() + "_" + center.north() + "_" + scale + "_" + 1158 getWidth() + "_" + getHeight() + "_" + getProjection().toString(); 1159 java.util.zip.CRC32 id = new java.util.zip.CRC32(); 1160 id.update(x.getBytes()); 1161 return (int)id.getValue(); 1162 } 1163 1164 public static SystemOfMeasurement getSystemOfMeasurement() { 1165 SystemOfMeasurement som = SYSTEMS_OF_MEASUREMENT.get(ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get()); 1166 if (som == null) 1167 return METRIC_SOM; 1168 return som; 1169 } 1170 1171 public static class SystemOfMeasurement { 1172 public final double aValue; 1173 public final double bValue; 1174 public final String aName; 1175 public final String bName; 1176 1177 /** 1178 * System of measurement. Currently covers only length units. 1179 * 1180 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as 1181 * x_a == x_m / aValue 1182 */ 1183 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName) { 1184 this.aValue = aValue; 1185 this.aName = aName; 1186 this.bValue = bValue; 1187 this.bName = bName; 1188 } 1189 1190 public String getDistText(double dist) { 1191 double a = dist / aValue; 1192 if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && a > bValue / aValue) { 1193 double b = dist / bValue; 1194 return String.format(Locale.US, "%." + (b<10 ? 2 : 1) + "f %s", b, bName); 1195 } else if (a < 0.01) 1196 return "< 0.01 " + aName; 1197 else 1198 return String.format(Locale.US, "%." + (a<10 ? 2 : 1) + "f %s", a, aName); 1199 } 1200 } 1201 1202 public static final SystemOfMeasurement METRIC_SOM = new SystemOfMeasurement(1, "m", 1000, "km"); 1203 public static final SystemOfMeasurement CHINESE_SOM = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */); 1204 public static final SystemOfMeasurement IMPERIAL_SOM = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi"); 1205 1206 public static final Map<String, SystemOfMeasurement> SYSTEMS_OF_MEASUREMENT; 1207 static { 1208 SYSTEMS_OF_MEASUREMENT = new LinkedHashMap<String, SystemOfMeasurement>(); 1209 SYSTEMS_OF_MEASUREMENT.put(marktr("Metric"), METRIC_SOM); 1210 SYSTEMS_OF_MEASUREMENT.put(marktr("Chinese"), CHINESE_SOM); 1211 SYSTEMS_OF_MEASUREMENT.put(marktr("Imperial"), IMPERIAL_SOM); 1212 } 1213 1214 private static class CursorInfo { 1215 public Cursor cursor; 1216 public Object object; 1217 public CursorInfo(Cursor c, Object o) { 1218 cursor = c; 1219 object = o; 1220 } 1221 } 1222 1223 private LinkedList<CursorInfo> Cursors = new LinkedList<CursorInfo>(); 1224 /** 1225 * Set new cursor. 1226 */ 1227 public void setNewCursor(Cursor cursor, Object reference) { 1228 if(Cursors.size() > 0) { 1229 CursorInfo l = Cursors.getLast(); 1230 if(l != null && l.cursor == cursor && l.object == reference) 1231 return; 1232 stripCursors(reference); 1233 } 1234 Cursors.add(new CursorInfo(cursor, reference)); 1235 setCursor(cursor); 1236 } 1237 public void setNewCursor(int cursor, Object reference) { 1238 setNewCursor(Cursor.getPredefinedCursor(cursor), reference); 1239 } 1240 /** 1241 * Remove the new cursor and reset to previous 1242 */ 1243 public void resetCursor(Object reference) { 1244 if(Cursors.size() == 0) { 1245 setCursor(null); 1246 return; 1247 } 1248 CursorInfo l = Cursors.getLast(); 1249 stripCursors(reference); 1250 if(l != null && l.object == reference) { 1251 if(Cursors.size() == 0) { 1252 setCursor(null); 1253 } else { 1254 setCursor(Cursors.getLast().cursor); 1255 } 1256 } 1257 } 1258 1259 private void stripCursors(Object reference) { 1260 LinkedList<CursorInfo> c = new LinkedList<CursorInfo>(); 1261 for(CursorInfo i : Cursors) { 1262 if(i.object != reference) { 1263 c.add(i); 1264 } 1265 } 1266 Cursors = c; 1267 } 1268 }