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

Last change on this file since 4067 was 4065, checked in by jttt, 13 years ago

Improved wms cache

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