source: josm/trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java@ 3718

Last change on this file since 3718 was 3652, checked in by bastiK, 13 years ago

fixed #5609 (patch by cmuelle8) - Unable to move node under another one

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