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

Last change on this file since 2759 was 2759, checked in by mjulius, 14 years ago

Make the new zoom previous and next actions listen to zoom changes and set enabled state.

  • Property svn:eol-style set to native
File size: 22.9 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.TreeMap;
15import java.util.concurrent.CopyOnWriteArrayList;
16
17import javax.swing.JComponent;
18
19import org.openstreetmap.josm.Main;
20import org.openstreetmap.josm.data.Bounds;
21import org.openstreetmap.josm.data.ProjectionBounds;
22import org.openstreetmap.josm.data.coor.CachedLatLon;
23import org.openstreetmap.josm.data.coor.EastNorth;
24import org.openstreetmap.josm.data.coor.LatLon;
25import org.openstreetmap.josm.data.osm.BBox;
26import org.openstreetmap.josm.data.osm.DataSet;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.Way;
30import org.openstreetmap.josm.data.osm.WaySegment;
31import org.openstreetmap.josm.data.projection.Projection;
32import org.openstreetmap.josm.gui.help.Helpful;
33
34/**
35 * An component that can be navigated by a mapmover. Used as map view and for the
36 * zoomer in the download dialog.
37 *
38 * @author imi
39 */
40public class NavigatableComponent extends JComponent implements Helpful {
41
42 /**
43 * Interface to notify listeners of the change of the zoom area.
44 */
45 public interface ZoomChangeListener {
46 void zoomChanged();
47 }
48
49 /**
50 * the zoom listeners
51 */
52 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>();
53
54 /**
55 * Removes a zoom change listener
56 *
57 * @param listener the listener. Ignored if null or already absent
58 */
59 public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
60 zoomChangeListeners.remove(listener);
61 }
62
63 /**
64 * Adds a zoom change listener
65 *
66 * @param listener the listener. Ignored if null or already registered.
67 */
68 public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
69 if (listener != null) {
70 zoomChangeListeners.addIfAbsent(listener);
71 }
72 }
73
74 protected static void fireZoomChanged() {
75 for (ZoomChangeListener l : zoomChangeListeners) {
76 l.zoomChanged();
77 }
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 LinkedList<ZoomData> zoomUndoBuffer = new LinkedList<ZoomData>();
384 private LinkedList<ZoomData> zoomRedoBuffer = new LinkedList<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.delay", 1.0) * 1000)) {
390 zoomUndoBuffer.push(new ZoomData(center, scale));
391 zoomRedoBuffer.clear();
392 }
393 zoomTimestamp = now;
394 }
395
396 public void zoomPrevious() {
397 if (!zoomUndoBuffer.isEmpty()) {
398 ZoomData zoom = zoomUndoBuffer.pop();
399 zoomRedoBuffer.push(new ZoomData(center, scale));
400 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
401 }
402 }
403
404 public void zoomNext() {
405 if (!zoomRedoBuffer.isEmpty()) {
406 ZoomData zoom = zoomRedoBuffer.pop();
407 zoomUndoBuffer.push(new ZoomData(center, scale));
408 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
409 }
410 }
411
412 public boolean hasZoomUndoEntries() {
413 return !zoomUndoBuffer.isEmpty();
414 }
415
416 public boolean hasZoomRedoEntries() {
417 return !zoomRedoBuffer.isEmpty();
418 }
419
420 private BBox getSnapDistanceBBox(Point p) {
421 return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
422 getLatLon(p.x + snapDistance, p.y + snapDistance));
423 }
424
425 /**
426 * Return the nearest point to the screen point given.
427 * If a node within snapDistance pixel is found, the nearest node is returned.
428 */
429 public final Node getNearestNode(Point p) {
430 DataSet ds = getCurrentDataSet();
431 if (ds == null)
432 return null;
433
434 double minDistanceSq = snapDistanceSq;
435 Node minPrimitive = null;
436 for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) {
437 if (!n.isUsable()) {
438 continue;
439 }
440 Point sp = getPoint(n);
441 double dist = p.distanceSq(sp);
442 if (dist < minDistanceSq) {
443 minDistanceSq = dist;
444 minPrimitive = n;
445 }
446 // when multiple nodes on one point, prefer new or selected nodes
447 else if (dist == minDistanceSq && minPrimitive != null
448 && ((n.isNew() && ds.isSelected(n))
449 || (!ds.isSelected(minPrimitive) && (ds.isSelected(n) || n.isNew())))) {
450 minPrimitive = n;
451 }
452 }
453 return minPrimitive;
454 }
455
456 /**
457 * @return all way segments within 10px of p, sorted by their
458 * perpendicular distance.
459 *
460 * @param p the point for which to search the nearest segment.
461 */
462 public final List<WaySegment> getNearestWaySegments(Point p) {
463 TreeMap<Double, List<WaySegment>> nearest = new TreeMap<Double, List<WaySegment>>();
464 DataSet ds = getCurrentDataSet();
465 if (ds == null)
466 return null;
467
468 for (Way w : ds.searchWays(getSnapDistanceBBox(p))) {
469 if (!w.isUsable()) {
470 continue;
471 }
472 Node lastN = null;
473 int i = -2;
474 for (Node n : w.getNodes()) {
475 i++;
476 if (n.isDeleted() || n.isIncomplete()) {
477 continue;
478 }
479 if (lastN == null) {
480 lastN = n;
481 continue;
482 }
483
484 Point A = getPoint(lastN);
485 Point B = getPoint(n);
486 double c = A.distanceSq(B);
487 double a = p.distanceSq(B);
488 double b = p.distanceSq(A);
489 double perDist = a - (a - b + c) * (a - b + c) / 4 / c; // perpendicular distance squared
490 if (perDist < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
491 if (ds.isSelected(w)) {
492 perDist -= 0.00001;
493 }
494 List<WaySegment> l;
495 if (nearest.containsKey(perDist)) {
496 l = nearest.get(perDist);
497 } else {
498 l = new LinkedList<WaySegment>();
499 nearest.put(perDist, l);
500 }
501 l.add(new WaySegment(w, i));
502 }
503
504 lastN = n;
505 }
506 }
507 ArrayList<WaySegment> nearestList = new ArrayList<WaySegment>();
508 for (List<WaySegment> wss : nearest.values()) {
509 nearestList.addAll(wss);
510 }
511 return nearestList;
512 }
513
514 /**
515 * @return the nearest way segment to the screen point given that is not
516 * in ignore.
517 *
518 * @param p the point for which to search the nearest segment.
519 * @param ignore a collection of segments which are not to be returned.
520 * May be null.
521 */
522 public final WaySegment getNearestWaySegment(Point p, Collection<WaySegment> ignore) {
523 List<WaySegment> nearest = getNearestWaySegments(p);
524 if(nearest == null)
525 return null;
526 if (ignore != null) {
527 nearest.removeAll(ignore);
528 }
529 return nearest.isEmpty() ? null : nearest.get(0);
530 }
531
532 /**
533 * @return the nearest way segment to the screen point given.
534 */
535 public final WaySegment getNearestWaySegment(Point p) {
536 return getNearestWaySegment(p, null);
537 }
538
539 /**
540 * @return the nearest way to the screen point given.
541 */
542 public final Way getNearestWay(Point p) {
543 WaySegment nearestWaySeg = getNearestWaySegment(p);
544 return nearestWaySeg == null ? null : nearestWaySeg.way;
545 }
546
547 /**
548 * Return the object, that is nearest to the given screen point.
549 *
550 * First, a node will be searched. If a node within 10 pixel is found, the
551 * nearest node is returned.
552 *
553 * If no node is found, search for near ways.
554 *
555 * If nothing is found, return <code>null</code>.
556 *
557 * @param p The point on screen.
558 * @return The primitive that is nearest to the point p.
559 */
560 public OsmPrimitive getNearest(Point p) {
561 OsmPrimitive osm = getNearestNode(p);
562 if (osm == null)
563 {
564 osm = getNearestWay(p);
565 }
566 return osm;
567 }
568
569 /**
570 * Returns a singleton of the nearest object, or else an empty collection.
571 */
572 public Collection<OsmPrimitive> getNearestCollection(Point p) {
573 OsmPrimitive osm = getNearest(p);
574 if (osm == null)
575 return Collections.emptySet();
576 return Collections.singleton(osm);
577 }
578
579 /**
580 * @return A list of all objects that are nearest to
581 * the mouse.
582 *
583 * @return A collection of all items or <code>null</code>
584 * if no item under or near the point. The returned
585 * list is never empty.
586 */
587 public Collection<OsmPrimitive> getAllNearest(Point p) {
588 Collection<OsmPrimitive> nearest = new HashSet<OsmPrimitive>();
589 DataSet ds = getCurrentDataSet();
590 if (ds == null)
591 return null;
592 for (Way w : ds.searchWays(getSnapDistanceBBox(p))) {
593 if (!w.isUsable()) {
594 continue;
595 }
596 Node lastN = null;
597 for (Node n : w.getNodes()) {
598 if (!n.isUsable()) {
599 continue;
600 }
601 if (lastN == null) {
602 lastN = n;
603 continue;
604 }
605 Point A = getPoint(lastN);
606 Point B = getPoint(n);
607 double c = A.distanceSq(B);
608 double a = p.distanceSq(B);
609 double b = p.distanceSq(A);
610 double perDist = a - (a - b + c) * (a - b + c) / 4 / c; // perpendicular distance squared
611 if (perDist < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
612 nearest.add(w);
613 break;
614 }
615 lastN = n;
616 }
617 }
618 for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) {
619 if (n.isUsable()
620 && getPoint(n).distanceSq(p) < snapDistanceSq) {
621 nearest.add(n);
622 }
623 }
624 return nearest.isEmpty() ? null : nearest;
625 }
626
627 /**
628 * @return A list of all nodes that are nearest to
629 * the mouse.
630 *
631 * @return A collection of all nodes or <code>null</code>
632 * if no node under or near the point. The returned
633 * list is never empty.
634 */
635 public Collection<Node> getNearestNodes(Point p) {
636 Collection<Node> nearest = new HashSet<Node>();
637 DataSet ds = getCurrentDataSet();
638 if (ds == null)
639 return null;
640
641 for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) {
642 if (n.isUsable()
643 && getPoint(n).distanceSq(p) < snapDistanceSq) {
644 nearest.add(n);
645 }
646 }
647 return nearest.isEmpty() ? null : nearest;
648 }
649
650 /**
651 * @return the nearest nodes to the screen point given that is not
652 * in ignore.
653 *
654 * @param p the point for which to search the nearest segment.
655 * @param ignore a collection of nodes which are not to be returned.
656 * May be null.
657 */
658 public final Collection<Node> getNearestNodes(Point p, Collection<Node> ignore) {
659 Collection<Node> nearest = getNearestNodes(p);
660 if (nearest == null) return null;
661 if (ignore != null) {
662 nearest.removeAll(ignore);
663 }
664 return nearest.isEmpty() ? null : nearest;
665 }
666
667 /**
668 * @return The projection to be used in calculating stuff.
669 */
670 public Projection getProjection() {
671 return Main.proj;
672 }
673
674 public String helpTopic() {
675 String n = getClass().getName();
676 return n.substring(n.lastIndexOf('.')+1);
677 }
678}
Note: See TracBrowser for help on using the repository browser.