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

Last change on this file since 3966 was 3919, checked in by stoecker, 13 years ago

unify cursor handling, hopefully fix #5381

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