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

Last change on this file since 1831 was 1823, checked in by stoecker, 15 years ago

some projection and zoom cleanups - projection classes still need better handling of outside-world coordinates

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