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

Last change on this file since 2606 was 2578, checked in by jttt, 14 years ago

Encalupse OsmPrimitive.incomplete

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