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

Last change on this file since 2876 was 2801, checked in by stoecker, 14 years ago

fixed line endings of recent checkins

  • Property svn:eol-style set to native
File size: 23.0 KB
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui;
4
5import java.awt.Point;
6import java.awt.Rectangle;
7import java.util.ArrayList;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.Date;
11import java.util.HashSet;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Stack;
15import java.util.TreeMap;
16import java.util.concurrent.CopyOnWriteArrayList;
17
18import javax.swing.JComponent;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.data.Bounds;
22import org.openstreetmap.josm.data.ProjectionBounds;
23import org.openstreetmap.josm.data.coor.CachedLatLon;
24import org.openstreetmap.josm.data.coor.EastNorth;
25import org.openstreetmap.josm.data.coor.LatLon;
26import org.openstreetmap.josm.data.osm.BBox;
27import org.openstreetmap.josm.data.osm.DataSet;
28import org.openstreetmap.josm.data.osm.Node;
29import org.openstreetmap.josm.data.osm.OsmPrimitive;
30import org.openstreetmap.josm.data.osm.Way;
31import org.openstreetmap.josm.data.osm.WaySegment;
32import org.openstreetmap.josm.data.projection.Projection;
33import org.openstreetmap.josm.gui.help.Helpful;
34
35/**
36 * An component that can be navigated by a mapmover. Used as map view and for the
37 * zoomer in the download dialog.
38 *
39 * @author imi
40 */
41public class NavigatableComponent extends JComponent implements Helpful {
42
43 /**
44 * Interface to notify listeners of the change of the zoom area.
45 */
46 public interface ZoomChangeListener {
47 void zoomChanged();
48 }
49
50 /**
51 * the zoom listeners
52 */
53 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>();
54
55 /**
56 * Removes a zoom change listener
57 *
58 * @param listener the listener. Ignored if null or already absent
59 */
60 public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
61 zoomChangeListeners.remove(listener);
62 }
63
64 /**
65 * Adds a zoom change listener
66 *
67 * @param listener the listener. Ignored if null or already registered.
68 */
69 public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
70 if (listener != null) {
71 zoomChangeListeners.addIfAbsent(listener);
72 }
73 }
74
75 protected static void fireZoomChanged() {
76 for (ZoomChangeListener l : zoomChangeListeners) {
77 l.zoomChanged();
78 }
79 }
80
81 public static final int snapDistance = Main.pref.getInteger("node.snap-distance", 10);
82 public static final int snapDistanceSq = sqr(snapDistance);
83
84 private static int sqr(int a) { return a*a;}
85 /**
86 * The scale factor in x or y-units per pixel. This means, if scale = 10,
87 * every physical pixel on screen are 10 x or 10 y units in the
88 * northing/easting space of the projection.
89 */
90 private double scale = Main.proj.getDefaultZoomInPPD();
91 /**
92 * Center n/e coordinate of the desired screen center.
93 */
94 protected EastNorth center = calculateDefaultCenter();
95
96 public NavigatableComponent() {
97 setLayout(null);
98 }
99
100 protected DataSet getCurrentDataSet() {
101 return Main.main.getCurrentDataSet();
102 }
103
104 private EastNorth calculateDefaultCenter() {
105 Bounds b = Main.proj.getWorldBoundsLatLon();
106 double lat = (b.getMax().lat() + b.getMin().lat())/2;
107 double lon = (b.getMax().lon() + b.getMin().lon())/2;
108
109 return Main.proj.latlon2eastNorth(new LatLon(lat, lon));
110 }
111
112 public String getDist100PixelText()
113 {
114 double dist = getDist100Pixel();
115 return dist >= 2000 ? Math.round(dist/100)/10 +" km" : (dist >= 1
116 ? Math.round(dist*10)/10 +" m" : "< 1 m");
117 }
118
119 public double getDist100Pixel()
120 {
121 int w = getWidth()/2;
122 int h = getHeight()/2;
123 LatLon ll1 = getLatLon(w-50,h);
124 LatLon ll2 = getLatLon(w+50,h);
125 return ll1.greatCircleDistance(ll2);
126 }
127
128 /**
129 * @return Returns the center point. A copy is returned, so users cannot
130 * change the center by accessing the return value. Use zoomTo instead.
131 */
132 public EastNorth getCenter() {
133 return center;
134 }
135
136 /**
137 * @param x X-Pixelposition to get coordinate from
138 * @param y Y-Pixelposition to get coordinate from
139 *
140 * @return Geographic coordinates from a specific pixel coordination
141 * on the screen.
142 */
143 public EastNorth getEastNorth(int x, int y) {
144 return new EastNorth(
145 center.east() + (x - getWidth()/2.0)*scale,
146 center.north() - (y - getHeight()/2.0)*scale);
147 }
148
149 public ProjectionBounds getProjectionBounds() {
150 return new ProjectionBounds(
151 new EastNorth(
152 center.east() - getWidth()/2.0*scale,
153 center.north() - getHeight()/2.0*scale),
154 new EastNorth(
155 center.east() + getWidth()/2.0*scale,
156 center.north() + getHeight()/2.0*scale));
157 }
158
159 /* FIXME: replace with better method - used by MapSlider */
160 public ProjectionBounds getMaxProjectionBounds() {
161 Bounds b = getProjection().getWorldBoundsLatLon();
162 return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
163 getProjection().latlon2eastNorth(b.getMax()));
164 }
165
166 /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
167 public Bounds getRealBounds() {
168 return new Bounds(
169 getProjection().eastNorth2latlon(new EastNorth(
170 center.east() - getWidth()/2.0*scale,
171 center.north() - getHeight()/2.0*scale)),
172 getProjection().eastNorth2latlon(new EastNorth(
173 center.east() + getWidth()/2.0*scale,
174 center.north() + getHeight()/2.0*scale)));
175 }
176
177 /**
178 * @param x X-Pixelposition to get coordinate from
179 * @param y Y-Pixelposition to get coordinate from
180 *
181 * @return Geographic unprojected coordinates from a specific pixel coordination
182 * on the screen.
183 */
184 public LatLon getLatLon(int x, int y) {
185 return getProjection().eastNorth2latlon(getEastNorth(x, y));
186 }
187
188 /**
189 * @param r
190 * @return Minimum bounds that will cover rectangle
191 */
192 public Bounds getLatLonBounds(Rectangle r) {
193 // TODO Maybe this should be (optional) method of Projection implementation
194 EastNorth p1 = getEastNorth(r.x, r.y);
195 EastNorth p2 = getEastNorth(r.x + r.width, r.y + r.height);
196
197 Bounds result = new Bounds(Main.proj.eastNorth2latlon(p1));
198
199 double eastMin = Math.min(p1.east(), p2.east());
200 double eastMax = Math.max(p1.east(), p2.east());
201 double northMin = Math.min(p1.north(), p2.north());
202 double northMax = Math.max(p1.north(), p2.north());
203 double deltaEast = (eastMax - eastMin) / 10;
204 double deltaNorth = (northMax - northMin) / 10;
205
206 for (int i=0; i < 10; i++) {
207 result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin)));
208 result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax)));
209 result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth)));
210 result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth)));
211 }
212
213 return result;
214 }
215
216 /**
217 * Return the point on the screen where this Coordinate would be.
218 * @param p The point, where this geopoint would be drawn.
219 * @return The point on screen where "point" would be drawn, relative
220 * to the own top/left.
221 */
222 public Point getPoint(EastNorth p) {
223 if (null == p)
224 return new Point();
225 double x = (p.east()-center.east())/scale + getWidth()/2;
226 double y = (center.north()-p.north())/scale + getHeight()/2;
227 return new Point((int)x,(int)y);
228 }
229
230 public Point getPoint(LatLon latlon) {
231 if (latlon == null)
232 return new Point();
233 else if (latlon instanceof CachedLatLon)
234 return getPoint(((CachedLatLon)latlon).getEastNorth());
235 else
236 return getPoint(getProjection().latlon2eastNorth(latlon));
237 }
238 public Point getPoint(Node n) {
239 return getPoint(n.getEastNorth());
240 }
241
242 /**
243 * Zoom to the given coordinate.
244 * @param newCenter The center x-value (easting) to zoom to.
245 * @param scale The scale to use.
246 */
247 private void zoomTo(EastNorth newCenter, double newScale) {
248 Bounds b = getProjection().getWorldBoundsLatLon();
249 CachedLatLon cl = new CachedLatLon(newCenter);
250 boolean changed = false;
251 double lat = cl.lat();
252 double lon = cl.lon();
253 if(lat < b.getMin().lat()) {changed = true; lat = b.getMin().lat(); }
254 else if(lat > b.getMax().lat()) {changed = true; lat = b.getMax().lat(); }
255 if(lon < b.getMin().lon()) {changed = true; lon = b.getMin().lon(); }
256 else if(lon > b.getMax().lon()) {changed = true; lon = b.getMax().lon(); }
257 if(changed) {
258 newCenter = new CachedLatLon(lat, lon).getEastNorth();
259 }
260 int width = getWidth()/2;
261 int height = getHeight()/2;
262 LatLon l1 = new LatLon(b.getMin().lat(), lon);
263 LatLon l2 = new LatLon(b.getMax().lat(), lon);
264 EastNorth e1 = getProjection().latlon2eastNorth(l1);
265 EastNorth e2 = getProjection().latlon2eastNorth(l2);
266 double d = e2.north() - e1.north();
267 if(d < height*newScale)
268 {
269 double newScaleH = d/height;
270 e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMin().lon()));
271 e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMax().lon()));
272 d = e2.east() - e1.east();
273 if(d < width*newScale) {
274 newScale = Math.max(newScaleH, d/width);
275 }
276 }
277 else
278 {
279 d = d/(l1.greatCircleDistance(l2)*height*10);
280 if(newScale < d) {
281 newScale = d;
282 }
283 }
284
285 if (!newCenter.equals(center) || (scale != newScale)) {
286 pushZoomUndo(center, scale);
287 zoomNoUndoTo(newCenter, newScale);
288 }
289 }
290
291 /**
292 * Zoom to the given coordinate without adding to the zoom undo buffer.
293 * @param newCenter The center x-value (easting) to zoom to.
294 * @param scale The scale to use.
295 */
296 private void zoomNoUndoTo(EastNorth newCenter, double newScale) {
297 if (!newCenter.equals(center)) {
298 EastNorth oldCenter = center;
299 center = newCenter;
300 firePropertyChange("center", oldCenter, newCenter);
301 }
302 if (scale != newScale) {
303 double oldScale = scale;
304 scale = newScale;
305 firePropertyChange("scale", oldScale, newScale);
306 }
307
308 repaint();
309 fireZoomChanged();
310 }
311
312 public void zoomTo(EastNorth newCenter) {
313 zoomTo(newCenter, scale);
314 }
315
316 public void zoomTo(LatLon newCenter) {
317 if(newCenter instanceof CachedLatLon) {
318 zoomTo(((CachedLatLon)newCenter).getEastNorth(), scale);
319 } else {
320 zoomTo(getProjection().latlon2eastNorth(newCenter), scale);
321 }
322 }
323
324 public void zoomToFactor(double x, double y, double factor) {
325 double newScale = scale*factor;
326 // New center position so that point under the mouse pointer stays the same place as it was before zooming
327 // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom
328 zoomTo(new EastNorth(
329 center.east() - (x - getWidth()/2.0) * (newScale - scale),
330 center.north() + (y - getHeight()/2.0) * (newScale - scale)),
331 newScale);
332 }
333
334 public void zoomToFactor(EastNorth newCenter, double factor) {
335 zoomTo(newCenter, scale*factor);
336 }
337
338 public void zoomToFactor(double factor) {
339 zoomTo(center, scale*factor);
340 }
341
342 public void zoomTo(ProjectionBounds box) {
343 // -20 to leave some border
344 int w = getWidth()-20;
345 if (w < 20) {
346 w = 20;
347 }
348 int h = getHeight()-20;
349 if (h < 20) {
350 h = 20;
351 }
352
353 double scaleX = (box.max.east()-box.min.east())/w;
354 double scaleY = (box.max.north()-box.min.north())/h;
355 double newScale = Math.max(scaleX, scaleY);
356
357 zoomTo(box.getCenter(), newScale);
358 }
359
360 public void zoomTo(Bounds box) {
361 zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
362 getProjection().latlon2eastNorth(box.getMax())));
363 }
364
365 private class ZoomData {
366 LatLon center;
367 double scale;
368
369 public ZoomData(EastNorth center, double scale) {
370 this.center = new CachedLatLon(center);
371 this.scale = scale;
372 }
373
374 public EastNorth getCenterEastNorth() {
375 return getProjection().latlon2eastNorth(center);
376 }
377
378 public double getScale() {
379 return scale;
380 }
381 }
382
383 private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>();
384 private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>();
385 private Date zoomTimestamp = new Date();
386
387 private void pushZoomUndo(EastNorth center, double scale) {
388 Date now = new Date();
389 if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {
390 zoomUndoBuffer.push(new ZoomData(center, scale));
391 if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {
392 zoomUndoBuffer.remove(0);
393 }
394 zoomRedoBuffer.clear();
395 }
396 zoomTimestamp = now;
397 }
398
399 public void zoomPrevious() {
400 if (!zoomUndoBuffer.isEmpty()) {
401 ZoomData zoom = zoomUndoBuffer.pop();
402 zoomRedoBuffer.push(new ZoomData(center, scale));
403 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
404 }
405 }
406
407 public void zoomNext() {
408 if (!zoomRedoBuffer.isEmpty()) {
409 ZoomData zoom = zoomRedoBuffer.pop();
410 zoomUndoBuffer.push(new ZoomData(center, scale));
411 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
412 }
413 }
414
415 public boolean hasZoomUndoEntries() {
416 return !zoomUndoBuffer.isEmpty();
417 }
418
419 public boolean hasZoomRedoEntries() {
420 return !zoomRedoBuffer.isEmpty();
421 }
422
423 private BBox getSnapDistanceBBox(Point p) {
424 return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
425 getLatLon(p.x + snapDistance, p.y + snapDistance));
426 }
427
428 /**
429 * Return the nearest point to the screen point given.
430 * If a node within snapDistance pixel is found, the nearest node is returned.
431 */
432 public final Node getNearestNode(Point p) {
433 DataSet ds = getCurrentDataSet();
434 if (ds == null)
435 return null;
436
437 double minDistanceSq = snapDistanceSq;
438 Node minPrimitive = null;
439 for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) {
440 if (!n.isUsable()) {
441 continue;
442 }
443 Point sp = getPoint(n);
444 double dist = p.distanceSq(sp);
445 if (dist < minDistanceSq) {
446 minDistanceSq = dist;
447 minPrimitive = n;
448 }
449 // when multiple nodes on one point, prefer new or selected nodes
450 else if (dist == minDistanceSq && minPrimitive != null
451 && ((n.isNew() && ds.isSelected(n))
452 || (!ds.isSelected(minPrimitive) && (ds.isSelected(n) || n.isNew())))) {
453 minPrimitive = n;
454 }
455 }
456 return minPrimitive;
457 }
458
459 /**
460 * @return all way segments within 10px of p, sorted by their
461 * perpendicular distance.
462 *
463 * @param p the point for which to search the nearest segment.
464 */
465 public final List<WaySegment> getNearestWaySegments(Point p) {
466 TreeMap<Double, List<WaySegment>> nearest = new TreeMap<Double, List<WaySegment>>();
467 DataSet ds = getCurrentDataSet();
468 if (ds == null)
469 return null;
470
471 for (Way w : ds.searchWays(getSnapDistanceBBox(p))) {
472 if (!w.isUsable()) {
473 continue;
474 }
475 Node lastN = null;
476 int i = -2;
477 for (Node n : w.getNodes()) {
478 i++;
479 if (n.isDeleted() || n.isIncomplete()) {
480 continue;
481 }
482 if (lastN == null) {
483 lastN = n;
484 continue;
485 }
486
487 Point A = getPoint(lastN);
488 Point B = getPoint(n);
489 double c = A.distanceSq(B);
490 double a = p.distanceSq(B);
491 double b = p.distanceSq(A);
492 double perDist = a - (a - b + c) * (a - b + c) / 4 / c; // perpendicular distance squared
493 if (perDist < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
494 if (ds.isSelected(w)) {
495 perDist -= 0.00001;
496 }
497 List<WaySegment> l;
498 if (nearest.containsKey(perDist)) {
499 l = nearest.get(perDist);
500 } else {
501 l = new LinkedList<WaySegment>();
502 nearest.put(perDist, l);
503 }
504 l.add(new WaySegment(w, i));
505 }
506
507 lastN = n;
508 }
509 }
510 ArrayList<WaySegment> nearestList = new ArrayList<WaySegment>();
511 for (List<WaySegment> wss : nearest.values()) {
512 nearestList.addAll(wss);
513 }
514 return nearestList;
515 }
516
517 /**
518 * @return the nearest way segment to the screen point given that is not
519 * in ignore.
520 *
521 * @param p the point for which to search the nearest segment.
522 * @param ignore a collection of segments which are not to be returned.
523 * May be null.
524 */
525 public final WaySegment getNearestWaySegment(Point p, Collection<WaySegment> ignore) {
526 List<WaySegment> nearest = getNearestWaySegments(p);
527 if(nearest == null)
528 return null;
529 if (ignore != null) {
530 nearest.removeAll(ignore);
531 }
532 return nearest.isEmpty() ? null : nearest.get(0);
533 }
534
535 /**
536 * @return the nearest way segment to the screen point given.
537 */
538 public final WaySegment getNearestWaySegment(Point p) {
539 return getNearestWaySegment(p, null);
540 }
541
542 /**
543 * @return the nearest way to the screen point given.
544 */
545 public final Way getNearestWay(Point p) {
546 WaySegment nearestWaySeg = getNearestWaySegment(p);
547 return nearestWaySeg == null ? null : nearestWaySeg.way;
548 }
549
550 /**
551 * Return the object, that is nearest to the given screen point.
552 *
553 * First, a node will be searched. If a node within 10 pixel is found, the
554 * nearest node is returned.
555 *
556 * If no node is found, search for near ways.
557 *
558 * If nothing is found, return <code>null</code>.
559 *
560 * @param p The point on screen.
561 * @return The primitive that is nearest to the point p.
562 */
563 public OsmPrimitive getNearest(Point p) {
564 OsmPrimitive osm = getNearestNode(p);
565 if (osm == null)
566 {
567 osm = getNearestWay(p);
568 }
569 return osm;
570 }
571
572 /**
573 * Returns a singleton of the nearest object, or else an empty collection.
574 */
575 public Collection<OsmPrimitive> getNearestCollection(Point p) {
576 OsmPrimitive osm = getNearest(p);
577 if (osm == null)
578 return Collections.emptySet();
579 return Collections.singleton(osm);
580 }
581
582 /**
583 * @return A list of all objects that are nearest to
584 * the mouse.
585 *
586 * @return A collection of all items or <code>null</code>
587 * if no item under or near the point. The returned
588 * list is never empty.
589 */
590 public Collection<OsmPrimitive> getAllNearest(Point p) {
591 Collection<OsmPrimitive> nearest = new HashSet<OsmPrimitive>();
592 DataSet ds = getCurrentDataSet();
593 if (ds == null)
594 return null;
595 for (Way w : ds.searchWays(getSnapDistanceBBox(p))) {
596 if (!w.isUsable()) {
597 continue;
598 }
599 Node lastN = null;
600 for (Node n : w.getNodes()) {
601 if (!n.isUsable()) {
602 continue;
603 }
604 if (lastN == null) {
605 lastN = n;
606 continue;
607 }
608 Point A = getPoint(lastN);
609 Point B = getPoint(n);
610 double c = A.distanceSq(B);
611 double a = p.distanceSq(B);
612 double b = p.distanceSq(A);
613 double perDist = a - (a - b + c) * (a - b + c) / 4 / c; // perpendicular distance squared
614 if (perDist < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
615 nearest.add(w);
616 break;
617 }
618 lastN = n;
619 }
620 }
621 for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) {
622 if (n.isUsable()
623 && getPoint(n).distanceSq(p) < snapDistanceSq) {
624 nearest.add(n);
625 }
626 }
627 return nearest.isEmpty() ? null : nearest;
628 }
629
630 /**
631 * @return A list of all nodes that are nearest to
632 * the mouse.
633 *
634 * @return A collection of all nodes or <code>null</code>
635 * if no node under or near the point. The returned
636 * list is never empty.
637 */
638 public Collection<Node> getNearestNodes(Point p) {
639 Collection<Node> nearest = new HashSet<Node>();
640 DataSet ds = getCurrentDataSet();
641 if (ds == null)
642 return null;
643
644 for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) {
645 if (n.isUsable()
646 && getPoint(n).distanceSq(p) < snapDistanceSq) {
647 nearest.add(n);
648 }
649 }
650 return nearest.isEmpty() ? null : nearest;
651 }
652
653 /**
654 * @return the nearest nodes to the screen point given that is not
655 * in ignore.
656 *
657 * @param p the point for which to search the nearest segment.
658 * @param ignore a collection of nodes which are not to be returned.
659 * May be null.
660 */
661 public final Collection<Node> getNearestNodes(Point p, Collection<Node> ignore) {
662 Collection<Node> nearest = getNearestNodes(p);
663 if (nearest == null) return null;
664 if (ignore != null) {
665 nearest.removeAll(ignore);
666 }
667 return nearest.isEmpty() ? null : nearest;
668 }
669
670 /**
671 * @return The projection to be used in calculating stuff.
672 */
673 public Projection getProjection() {
674 return Main.proj;
675 }
676
677 public String helpTopic() {
678 String n = getClass().getName();
679 return n.substring(n.lastIndexOf('.')+1);
680 }
681}
Note: See TracBrowser for help on using the repository browser.