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

Last change on this file since 6789 was 6509, checked in by Don-vip, 10 years ago

fix #9459 - initialize default center view of MapView to last download location, if any, instead of (0,0)

  • Property svn:eol-style set to native
File size: 62.9 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.Graphics;
8import java.awt.Point;
9import java.awt.Polygon;
10import java.awt.Rectangle;
11import java.awt.geom.AffineTransform;
12import java.awt.geom.Point2D;
13import java.text.NumberFormat;
14import java.util.ArrayList;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.Date;
18import java.util.HashSet;
19import java.util.LinkedHashMap;
20import java.util.LinkedList;
21import java.util.List;
22import java.util.Locale;
23import java.util.Map;
24import java.util.Map.Entry;
25import java.util.Set;
26import java.util.Stack;
27import java.util.TreeMap;
28import java.util.concurrent.CopyOnWriteArrayList;
29
30import javax.swing.JComponent;
31
32import org.openstreetmap.josm.Main;
33import org.openstreetmap.josm.data.Bounds;
34import org.openstreetmap.josm.data.ProjectionBounds;
35import org.openstreetmap.josm.data.coor.CachedLatLon;
36import org.openstreetmap.josm.data.coor.EastNorth;
37import org.openstreetmap.josm.data.coor.LatLon;
38import org.openstreetmap.josm.data.osm.BBox;
39import org.openstreetmap.josm.data.osm.DataSet;
40import org.openstreetmap.josm.data.osm.Node;
41import org.openstreetmap.josm.data.osm.OsmPrimitive;
42import org.openstreetmap.josm.data.osm.Relation;
43import org.openstreetmap.josm.data.osm.Way;
44import org.openstreetmap.josm.data.osm.WaySegment;
45import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
46import org.openstreetmap.josm.data.preferences.IntegerProperty;
47import org.openstreetmap.josm.data.projection.Projection;
48import org.openstreetmap.josm.data.projection.Projections;
49import org.openstreetmap.josm.gui.download.DownloadDialog;
50import org.openstreetmap.josm.gui.help.Helpful;
51import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
52import org.openstreetmap.josm.tools.Predicate;
53import org.openstreetmap.josm.tools.Utils;
54
55/**
56 * An component that can be navigated by a mapmover. Used as map view and for the
57 * zoomer in the download dialog.
58 *
59 * @author imi
60 */
61public class NavigatableComponent extends JComponent implements Helpful {
62
63 /**
64 * Interface to notify listeners of the change of the zoom area.
65 */
66 public interface ZoomChangeListener {
67 void zoomChanged();
68 }
69
70 /**
71 * Interface to notify listeners of the change of the system of measurement.
72 * @since 6056
73 */
74 public interface SoMChangeListener {
75 /**
76 * The current SoM has changed.
77 * @param oldSoM The old system of measurement
78 * @param newSoM The new (current) system of measurement
79 */
80 void systemOfMeasurementChanged(String oldSoM, String newSoM);
81 }
82
83 /**
84 * Simple data class that keeps map center and scale in one object.
85 */
86 public static class ViewportData {
87 private EastNorth center;
88 private Double scale;
89
90 public ViewportData(EastNorth center, Double scale) {
91 this.center = center;
92 this.scale = scale;
93 }
94
95 /**
96 * Return the projected coordinates of the map center
97 * @return the center
98 */
99 public EastNorth getCenter() {
100 return center;
101 }
102
103 /**
104 * Return the scale factor in east-/north-units per pixel.
105 * @return the scale
106 */
107 public Double getScale() {
108 return scale;
109 }
110 }
111
112 public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
113
114 public static final String PROPNAME_CENTER = "center";
115 public static final String PROPNAME_SCALE = "scale";
116
117 /**
118 * the zoom listeners
119 */
120 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>();
121
122 /**
123 * Removes a zoom change listener
124 *
125 * @param listener the listener. Ignored if null or already absent
126 */
127 public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
128 zoomChangeListeners.remove(listener);
129 }
130
131 /**
132 * Adds a zoom change listener
133 *
134 * @param listener the listener. Ignored if null or already registered.
135 */
136 public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
137 if (listener != null) {
138 zoomChangeListeners.addIfAbsent(listener);
139 }
140 }
141
142 protected static void fireZoomChanged() {
143 for (ZoomChangeListener l : zoomChangeListeners) {
144 l.zoomChanged();
145 }
146 }
147
148 /**
149 * the SoM listeners
150 */
151 private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<SoMChangeListener>();
152
153 /**
154 * Removes a SoM change listener
155 *
156 * @param listener the listener. Ignored if null or already absent
157 * @since 6056
158 */
159 public static void removeSoMChangeListener(NavigatableComponent.SoMChangeListener listener) {
160 somChangeListeners.remove(listener);
161 }
162
163 /**
164 * Adds a SoM change listener
165 *
166 * @param listener the listener. Ignored if null or already registered.
167 * @since 6056
168 */
169 public static void addSoMChangeListener(NavigatableComponent.SoMChangeListener listener) {
170 if (listener != null) {
171 somChangeListeners.addIfAbsent(listener);
172 }
173 }
174
175 protected static void fireSoMChanged(String oldSoM, String newSoM) {
176 for (SoMChangeListener l : somChangeListeners) {
177 l.systemOfMeasurementChanged(oldSoM, newSoM);
178 }
179 }
180
181 /**
182 * The scale factor in x or y-units per pixel. This means, if scale = 10,
183 * every physical pixel on screen are 10 x or 10 y units in the
184 * northing/easting space of the projection.
185 */
186 private double scale = Main.getProjection().getDefaultZoomInPPD();
187 /**
188 * Center n/e coordinate of the desired screen center.
189 */
190 protected EastNorth center = calculateDefaultCenter();
191
192 private final Object paintRequestLock = new Object();
193 private Rectangle paintRect = null;
194 private Polygon paintPoly = null;
195
196 /**
197 * Constructs a new {@code NavigatableComponent}.
198 */
199 public NavigatableComponent() {
200 setLayout(null);
201 }
202
203 protected DataSet getCurrentDataSet() {
204 return Main.main.getCurrentDataSet();
205 }
206
207 private EastNorth calculateDefaultCenter() {
208 Bounds b = DownloadDialog.getSavedDownloadBounds();
209 if (b == null) {
210 b = Main.getProjection().getWorldBoundsLatLon();
211 }
212 return Main.getProjection().latlon2eastNorth(b.getCenter());
213 }
214
215 /**
216 * Returns the text describing the given distance in the current system of measurement.
217 * @param dist The distance in metres.
218 * @return the text describing the given distance in the current system of measurement.
219 * @since 3406
220 */
221 public static String getDistText(double dist) {
222 return getSystemOfMeasurement().getDistText(dist);
223 }
224
225 /**
226 * Returns the text describing the given area in the current system of measurement.
227 * @param area The distance in square metres.
228 * @return the text describing the given area in the current system of measurement.
229 * @since 5560
230 */
231 public static String getAreaText(double area) {
232 return getSystemOfMeasurement().getAreaText(area);
233 }
234
235 public String getDist100PixelText()
236 {
237 return getDistText(getDist100Pixel());
238 }
239
240 public double getDist100Pixel()
241 {
242 int w = getWidth()/2;
243 int h = getHeight()/2;
244 LatLon ll1 = getLatLon(w-50,h);
245 LatLon ll2 = getLatLon(w+50,h);
246 return ll1.greatCircleDistance(ll2);
247 }
248
249 /**
250 * @return Returns the center point. A copy is returned, so users cannot
251 * change the center by accessing the return value. Use zoomTo instead.
252 */
253 public EastNorth getCenter() {
254 return center;
255 }
256
257 public double getScale() {
258 return scale;
259 }
260
261 /**
262 * @param x X-Pixelposition to get coordinate from
263 * @param y Y-Pixelposition to get coordinate from
264 *
265 * @return Geographic coordinates from a specific pixel coordination
266 * on the screen.
267 */
268 public EastNorth getEastNorth(int x, int y) {
269 return new EastNorth(
270 center.east() + (x - getWidth()/2.0)*scale,
271 center.north() - (y - getHeight()/2.0)*scale);
272 }
273
274 public ProjectionBounds getProjectionBounds() {
275 return new ProjectionBounds(
276 new EastNorth(
277 center.east() - getWidth()/2.0*scale,
278 center.north() - getHeight()/2.0*scale),
279 new EastNorth(
280 center.east() + getWidth()/2.0*scale,
281 center.north() + getHeight()/2.0*scale));
282 }
283
284 /* FIXME: replace with better method - used by MapSlider */
285 public ProjectionBounds getMaxProjectionBounds() {
286 Bounds b = getProjection().getWorldBoundsLatLon();
287 return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
288 getProjection().latlon2eastNorth(b.getMax()));
289 }
290
291 /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
292 public Bounds getRealBounds() {
293 return new Bounds(
294 getProjection().eastNorth2latlon(new EastNorth(
295 center.east() - getWidth()/2.0*scale,
296 center.north() - getHeight()/2.0*scale)),
297 getProjection().eastNorth2latlon(new EastNorth(
298 center.east() + getWidth()/2.0*scale,
299 center.north() + getHeight()/2.0*scale)));
300 }
301
302 /**
303 * @param x X-Pixelposition to get coordinate from
304 * @param y Y-Pixelposition to get coordinate from
305 *
306 * @return Geographic unprojected coordinates from a specific pixel coordination
307 * on the screen.
308 */
309 public LatLon getLatLon(int x, int y) {
310 return getProjection().eastNorth2latlon(getEastNorth(x, y));
311 }
312
313 public LatLon getLatLon(double x, double y) {
314 return getLatLon((int)x, (int)y);
315 }
316
317 /**
318 * @param r
319 * @return Minimum bounds that will cover rectangle
320 */
321 public Bounds getLatLonBounds(Rectangle r) {
322 // TODO Maybe this should be (optional) method of Projection implementation
323 EastNorth p1 = getEastNorth(r.x, r.y);
324 EastNorth p2 = getEastNorth(r.x + r.width, r.y + r.height);
325
326 Bounds result = new Bounds(Main.getProjection().eastNorth2latlon(p1));
327
328 double eastMin = Math.min(p1.east(), p2.east());
329 double eastMax = Math.max(p1.east(), p2.east());
330 double northMin = Math.min(p1.north(), p2.north());
331 double northMax = Math.max(p1.north(), p2.north());
332 double deltaEast = (eastMax - eastMin) / 10;
333 double deltaNorth = (northMax - northMin) / 10;
334
335 for (int i=0; i < 10; i++) {
336 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin)));
337 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax)));
338 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth)));
339 result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth)));
340 }
341
342 return result;
343 }
344
345 public AffineTransform getAffineTransform() {
346 return new AffineTransform(
347 1.0/scale, 0.0, 0.0, -1.0/scale, getWidth()/2.0 - center.east()/scale, getHeight()/2.0 + center.north()/scale);
348 }
349
350 /**
351 * Return the point on the screen where this Coordinate would be.
352 * @param p The point, where this geopoint would be drawn.
353 * @return The point on screen where "point" would be drawn, relative
354 * to the own top/left.
355 */
356 public Point2D getPoint2D(EastNorth p) {
357 if (null == p)
358 return new Point();
359 double x = (p.east()-center.east())/scale + getWidth()/2;
360 double y = (center.north()-p.north())/scale + getHeight()/2;
361 return new Point2D.Double(x, y);
362 }
363
364 public Point2D getPoint2D(LatLon latlon) {
365 if (latlon == null)
366 return new Point();
367 else if (latlon instanceof CachedLatLon)
368 return getPoint2D(((CachedLatLon)latlon).getEastNorth());
369 else
370 return getPoint2D(getProjection().latlon2eastNorth(latlon));
371 }
372
373 public Point2D getPoint2D(Node n) {
374 return getPoint2D(n.getEastNorth());
375 }
376
377 // looses precision, may overflow (depends on p and current scale)
378 //@Deprecated
379 public Point getPoint(EastNorth p) {
380 Point2D d = getPoint2D(p);
381 return new Point((int) d.getX(), (int) d.getY());
382 }
383
384 // looses precision, may overflow (depends on p and current scale)
385 //@Deprecated
386 public Point getPoint(LatLon latlon) {
387 Point2D d = getPoint2D(latlon);
388 return new Point((int) d.getX(), (int) d.getY());
389 }
390
391 // looses precision, may overflow (depends on p and current scale)
392 //@Deprecated
393 public Point getPoint(Node n) {
394 Point2D d = getPoint2D(n);
395 return new Point((int) d.getX(), (int) d.getY());
396 }
397
398 /**
399 * Zoom to the given coordinate.
400 * @param newCenter The center x-value (easting) to zoom to.
401 * @param newScale The scale to use.
402 */
403 public void zoomTo(EastNorth newCenter, double newScale) {
404 Bounds b = getProjection().getWorldBoundsLatLon();
405 LatLon cl = Projections.inverseProject(newCenter);
406 boolean changed = false;
407 double lat = cl.lat();
408 double lon = cl.lon();
409 if(lat < b.getMinLat()) {changed = true; lat = b.getMinLat(); }
410 else if(lat > b.getMaxLat()) {changed = true; lat = b.getMaxLat(); }
411 if(lon < b.getMinLon()) {changed = true; lon = b.getMinLon(); }
412 else if(lon > b.getMaxLon()) {changed = true; lon = b.getMaxLon(); }
413 if(changed) {
414 newCenter = Projections.project(new LatLon(lat,lon));
415 }
416 int width = getWidth()/2;
417 int height = getHeight()/2;
418 if (width == 0 || height == 0) {
419 throw new IllegalStateException("Cannot zoom into undimensioned NavigatableComponent");
420 }
421 LatLon l1 = new LatLon(b.getMinLat(), lon);
422 LatLon l2 = new LatLon(b.getMaxLat(), lon);
423 EastNorth e1 = getProjection().latlon2eastNorth(l1);
424 EastNorth e2 = getProjection().latlon2eastNorth(l2);
425 double d = e2.north() - e1.north();
426 if(d < height*newScale)
427 {
428 double newScaleH = d/height;
429 e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMinLon()));
430 e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMaxLon()));
431 d = e2.east() - e1.east();
432 if(d < width*newScale) {
433 newScale = Math.max(newScaleH, d/width);
434 }
435 }
436 else
437 {
438 d = d/(l1.greatCircleDistance(l2)*height*10);
439 if(newScale < d) {
440 newScale = d;
441 }
442 }
443
444 if (!newCenter.equals(center) || (scale != newScale)) {
445 pushZoomUndo(center, scale);
446 zoomNoUndoTo(newCenter, newScale);
447 }
448 }
449
450 /**
451 * Zoom to the given coordinate without adding to the zoom undo buffer.
452 * @param newCenter The center x-value (easting) to zoom to.
453 * @param newScale The scale to use.
454 */
455 private void zoomNoUndoTo(EastNorth newCenter, double newScale) {
456 if (!newCenter.equals(center)) {
457 EastNorth oldCenter = center;
458 center = newCenter;
459 firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter);
460 }
461 if (scale != newScale) {
462 double oldScale = scale;
463 scale = newScale;
464 firePropertyChange(PROPNAME_SCALE, oldScale, newScale);
465 }
466
467 repaint();
468 fireZoomChanged();
469 }
470
471 public void zoomTo(EastNorth newCenter) {
472 zoomTo(newCenter, scale);
473 }
474
475 public void zoomTo(LatLon newCenter) {
476 zoomTo(Projections.project(newCenter));
477 }
478
479 public void smoothScrollTo(LatLon newCenter) {
480 smoothScrollTo(Projections.project(newCenter));
481 }
482
483 /**
484 * Create a thread that moves the viewport to the given center in an
485 * animated fashion.
486 */
487 public void smoothScrollTo(EastNorth newCenter) {
488 // FIXME make these configurable.
489 final int fps = 20; // animation frames per second
490 final int speed = 1500; // milliseconds for full-screen-width pan
491 if (!newCenter.equals(center)) {
492 final EastNorth oldCenter = center;
493 final double distance = newCenter.distance(oldCenter) / scale;
494 final double milliseconds = distance / getWidth() * speed;
495 final double frames = milliseconds * fps / 1000;
496 final EastNorth finalNewCenter = newCenter;
497
498 new Thread(){
499 @Override
500 public void run() {
501 for (int i=0; i<frames; i++) {
502 // FIXME - not use zoom history here
503 zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames));
504 try {
505 Thread.sleep(1000 / fps);
506 } catch (InterruptedException ex) {
507 Main.warn("InterruptedException in "+NavigatableComponent.class.getSimpleName()+" during smooth scrolling");
508 }
509 }
510 }
511 }.start();
512 }
513 }
514
515 public void zoomToFactor(double x, double y, double factor) {
516 double newScale = scale*factor;
517 // New center position so that point under the mouse pointer stays the same place as it was before zooming
518 // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom
519 zoomTo(new EastNorth(
520 center.east() - (x - getWidth()/2.0) * (newScale - scale),
521 center.north() + (y - getHeight()/2.0) * (newScale - scale)),
522 newScale);
523 }
524
525 public void zoomToFactor(EastNorth newCenter, double factor) {
526 zoomTo(newCenter, scale*factor);
527 }
528
529 public void zoomToFactor(double factor) {
530 zoomTo(center, scale*factor);
531 }
532
533 public void zoomTo(ProjectionBounds box) {
534 // -20 to leave some border
535 int w = getWidth()-20;
536 if (w < 20) {
537 w = 20;
538 }
539 int h = getHeight()-20;
540 if (h < 20) {
541 h = 20;
542 }
543
544 double scaleX = (box.maxEast-box.minEast)/w;
545 double scaleY = (box.maxNorth-box.minNorth)/h;
546 double newScale = Math.max(scaleX, scaleY);
547
548 zoomTo(box.getCenter(), newScale);
549 }
550
551 public void zoomTo(Bounds box) {
552 zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
553 getProjection().latlon2eastNorth(box.getMax())));
554 }
555
556 private class ZoomData {
557 LatLon center;
558 double scale;
559
560 public ZoomData(EastNorth center, double scale) {
561 this.center = Projections.inverseProject(center);
562 this.scale = scale;
563 }
564
565 public EastNorth getCenterEastNorth() {
566 return getProjection().latlon2eastNorth(center);
567 }
568
569 public double getScale() {
570 return scale;
571 }
572 }
573
574 private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>();
575 private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>();
576 private Date zoomTimestamp = new Date();
577
578 private void pushZoomUndo(EastNorth center, double scale) {
579 Date now = new Date();
580 if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {
581 zoomUndoBuffer.push(new ZoomData(center, scale));
582 if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {
583 zoomUndoBuffer.remove(0);
584 }
585 zoomRedoBuffer.clear();
586 }
587 zoomTimestamp = now;
588 }
589
590 public void zoomPrevious() {
591 if (!zoomUndoBuffer.isEmpty()) {
592 ZoomData zoom = zoomUndoBuffer.pop();
593 zoomRedoBuffer.push(new ZoomData(center, scale));
594 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
595 }
596 }
597
598 public void zoomNext() {
599 if (!zoomRedoBuffer.isEmpty()) {
600 ZoomData zoom = zoomRedoBuffer.pop();
601 zoomUndoBuffer.push(new ZoomData(center, scale));
602 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
603 }
604 }
605
606 public boolean hasZoomUndoEntries() {
607 return !zoomUndoBuffer.isEmpty();
608 }
609
610 public boolean hasZoomRedoEntries() {
611 return !zoomRedoBuffer.isEmpty();
612 }
613
614 private BBox getBBox(Point p, int snapDistance) {
615 return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
616 getLatLon(p.x + snapDistance, p.y + snapDistance));
617 }
618
619 /**
620 * The *result* does not depend on the current map selection state,
621 * neither does the result *order*.
622 * It solely depends on the distance to point p.
623 *
624 * @return a sorted map with the keys representing the distance of
625 * their associated nodes to point p.
626 */
627 private Map<Double, List<Node>> getNearestNodesImpl(Point p,
628 Predicate<OsmPrimitive> predicate) {
629 TreeMap<Double, List<Node>> nearestMap = new TreeMap<Double, List<Node>>();
630 DataSet ds = getCurrentDataSet();
631
632 if (ds != null) {
633 double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
634 snapDistanceSq *= snapDistanceSq;
635
636 for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
637 if (predicate.evaluate(n)
638 && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq)
639 {
640 List<Node> nlist;
641 if (nearestMap.containsKey(dist)) {
642 nlist = nearestMap.get(dist);
643 } else {
644 nlist = new LinkedList<Node>();
645 nearestMap.put(dist, nlist);
646 }
647 nlist.add(n);
648 }
649 }
650 }
651
652 return nearestMap;
653 }
654
655 /**
656 * The *result* does not depend on the current map selection state,
657 * neither does the result *order*.
658 * It solely depends on the distance to point p.
659 *
660 * @return All nodes nearest to point p that are in a belt from
661 * dist(nearest) to dist(nearest)+4px around p and
662 * that are not in ignore.
663 *
664 * @param p the point for which to search the nearest segment.
665 * @param ignore a collection of nodes which are not to be returned.
666 * @param predicate the returned objects have to fulfill certain properties.
667 */
668 public final List<Node> getNearestNodes(Point p,
669 Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
670 List<Node> nearestList = Collections.emptyList();
671
672 if (ignore == null) {
673 ignore = Collections.emptySet();
674 }
675
676 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
677 if (!nlists.isEmpty()) {
678 Double minDistSq = null;
679 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
680 Double distSq = entry.getKey();
681 List<Node> nlist = entry.getValue();
682
683 // filter nodes to be ignored before determining minDistSq..
684 nlist.removeAll(ignore);
685 if (minDistSq == null) {
686 if (!nlist.isEmpty()) {
687 minDistSq = distSq;
688 nearestList = new ArrayList<Node>();
689 nearestList.addAll(nlist);
690 }
691 } else {
692 if (distSq-minDistSq < (4)*(4)) {
693 nearestList.addAll(nlist);
694 }
695 }
696 }
697 }
698
699 return nearestList;
700 }
701
702 /**
703 * The *result* does not depend on the current map selection state,
704 * neither does the result *order*.
705 * It solely depends on the distance to point p.
706 *
707 * @return All nodes nearest to point p that are in a belt from
708 * dist(nearest) to dist(nearest)+4px around p.
709 * @see #getNearestNodes(Point, Collection, Predicate)
710 *
711 * @param p the point for which to search the nearest segment.
712 * @param predicate the returned objects have to fulfill certain properties.
713 */
714 public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
715 return getNearestNodes(p, null, predicate);
716 }
717
718 /**
719 * The *result* depends on the current map selection state IF use_selected is true.
720 *
721 * If more than one node within node.snap-distance pixels is found,
722 * the nearest node selected is returned IF use_selected is true.
723 *
724 * Else the nearest new/id=0 node within about the same distance
725 * as the true nearest node is returned.
726 *
727 * If no such node is found either, the true nearest
728 * node to p is returned.
729 *
730 * Finally, if a node is not found at all, null is returned.
731 *
732 * @return A node within snap-distance to point p,
733 * that is chosen by the algorithm described.
734 *
735 * @param p the screen point
736 * @param predicate this parameter imposes a condition on the returned object, e.g.
737 * give the nearest node that is tagged.
738 */
739 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) {
740 return getNearestNode(p, predicate, use_selected, null);
741 }
742
743 /**
744 * The *result* depends on the current map selection state IF use_selected is true
745 *
746 * If more than one node within node.snap-distance pixels is found,
747 * the nearest node selected is returned IF use_selected is true.
748 *
749 * If there are no selected nodes near that point, the node that is related to some of the preferredRefs
750 *
751 * Else the nearest new/id=0 node within about the same distance
752 * as the true nearest node is returned.
753 *
754 * If no such node is found either, the true nearest
755 * node to p is returned.
756 *
757 * Finally, if a node is not found at all, null is returned.
758 * @since 6065
759 * @return A node within snap-distance to point p,
760 * that is chosen by the algorithm described.
761 *
762 * @param p the screen point
763 * @param predicate this parameter imposes a condition on the returned object, e.g.
764 * give the nearest node that is tagged.
765 * @param preferredRefs primitives, whose nodes we prefer
766 */
767 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
768 boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
769
770 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
771 if (nlists.isEmpty()) return null;
772
773 if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
774 Node ntsel = null, ntnew = null, ntref = null;
775 boolean useNtsel = useSelected;
776 double minDistSq = nlists.keySet().iterator().next();
777
778 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
779 Double distSq = entry.getKey();
780 for (Node nd : entry.getValue()) {
781 // find the nearest selected node
782 if (ntsel == null && nd.isSelected()) {
783 ntsel = nd;
784 // if there are multiple nearest nodes, prefer the one
785 // that is selected. This is required in order to drag
786 // the selected node if multiple nodes have the same
787 // coordinates (e.g. after unglue)
788 useNtsel |= (distSq == minDistSq);
789 }
790 if (ntref == null && preferredRefs != null && distSq == minDistSq) {
791 List<OsmPrimitive> ndRefs = nd.getReferrers();
792 for (OsmPrimitive ref: preferredRefs) {
793 if (ndRefs.contains(ref)) {
794 ntref = nd;
795 break;
796 }
797 }
798 }
799 // find the nearest newest node that is within about the same
800 // distance as the true nearest node
801 if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
802 ntnew = nd;
803 }
804 }
805 }
806
807 // take nearest selected, nearest new or true nearest node to p, in that order
808 if (ntsel != null && useNtsel)
809 return ntsel;
810 if (ntref != null)
811 return ntref;
812 if (ntnew != null)
813 return ntnew;
814 return nlists.values().iterator().next().get(0);
815 }
816
817 /**
818 * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
819 *
820 * @return The nearest node to point p.
821 */
822 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
823 return getNearestNode(p, predicate, true);
824 }
825
826 /**
827 * The *result* does not depend on the current map selection state,
828 * neither does the result *order*.
829 * It solely depends on the distance to point p.
830 *
831 * @return a sorted map with the keys representing the perpendicular
832 * distance of their associated way segments to point p.
833 */
834 private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p,
835 Predicate<OsmPrimitive> predicate) {
836 Map<Double, List<WaySegment>> nearestMap = new TreeMap<Double, List<WaySegment>>();
837 DataSet ds = getCurrentDataSet();
838
839 if (ds != null) {
840 double snapDistanceSq = Main.pref.getInteger("mappaint.segment.snap-distance", 10);
841 snapDistanceSq *= snapDistanceSq;
842
843 for (Way w : ds.searchWays(getBBox(p, Main.pref.getInteger("mappaint.segment.snap-distance", 10)))) {
844 if (!predicate.evaluate(w)) {
845 continue;
846 }
847 Node lastN = null;
848 int i = -2;
849 for (Node n : w.getNodes()) {
850 i++;
851 if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
852 continue;
853 }
854 if (lastN == null) {
855 lastN = n;
856 continue;
857 }
858
859 Point2D A = getPoint2D(lastN);
860 Point2D B = getPoint2D(n);
861 double c = A.distanceSq(B);
862 double a = p.distanceSq(B);
863 double b = p.distanceSq(A);
864
865 /* perpendicular distance squared
866 * loose some precision to account for possible deviations in the calculation above
867 * e.g. if identical (A and B) come about reversed in another way, values may differ
868 * -- zero out least significant 32 dual digits of mantissa..
869 */
870 double perDistSq = Double.longBitsToDouble(
871 Double.doubleToLongBits( a - (a - b + c) * (a - b + c) / 4 / c )
872 >> 32 << 32); // resolution in numbers with large exponent not needed here..
873
874 if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
875 List<WaySegment> wslist;
876 if (nearestMap.containsKey(perDistSq)) {
877 wslist = nearestMap.get(perDistSq);
878 } else {
879 wslist = new LinkedList<WaySegment>();
880 nearestMap.put(perDistSq, wslist);
881 }
882 wslist.add(new WaySegment(w, i));
883 }
884
885 lastN = n;
886 }
887 }
888 }
889
890 return nearestMap;
891 }
892
893 /**
894 * The result *order* depends on the current map selection state.
895 * Segments within 10px of p are searched and sorted by their distance to @param p,
896 * then, within groups of equally distant segments, prefer those that are selected.
897 *
898 * @return all segments within 10px of p that are not in ignore,
899 * sorted by their perpendicular distance.
900 *
901 * @param p the point for which to search the nearest segments.
902 * @param ignore a collection of segments which are not to be returned.
903 * @param predicate the returned objects have to fulfill certain properties.
904 */
905 public final List<WaySegment> getNearestWaySegments(Point p,
906 Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
907 List<WaySegment> nearestList = new ArrayList<WaySegment>();
908 List<WaySegment> unselected = new LinkedList<WaySegment>();
909
910 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
911 // put selected waysegs within each distance group first
912 // makes the order of nearestList dependent on current selection state
913 for (WaySegment ws : wss) {
914 (ws.way.isSelected() ? nearestList : unselected).add(ws);
915 }
916 nearestList.addAll(unselected);
917 unselected.clear();
918 }
919 if (ignore != null) {
920 nearestList.removeAll(ignore);
921 }
922
923 return nearestList;
924 }
925
926 /**
927 * The result *order* depends on the current map selection state.
928 *
929 * @return all segments within 10px of p, sorted by their perpendicular distance.
930 * @see #getNearestWaySegments(Point, Collection, Predicate)
931 *
932 * @param p the point for which to search the nearest segments.
933 * @param predicate the returned objects have to fulfill certain properties.
934 */
935 public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
936 return getNearestWaySegments(p, null, predicate);
937 }
938
939 /**
940 * The *result* depends on the current map selection state IF use_selected is true.
941 *
942 * @return The nearest way segment to point p,
943 * and, depending on use_selected, prefers a selected way segment, if found.
944 * @see #getNearestWaySegments(Point, Collection, Predicate)
945 *
946 * @param p the point for which to search the nearest segment.
947 * @param predicate the returned object has to fulfill certain properties.
948 * @param use_selected whether selected way segments should be preferred.
949 */
950 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) {
951 WaySegment wayseg = null, ntsel = null;
952
953 for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
954 if (wayseg != null && ntsel != null) {
955 break;
956 }
957 for (WaySegment ws : wslist) {
958 if (wayseg == null) {
959 wayseg = ws;
960 }
961 if (ntsel == null && ws.way.isSelected()) {
962 ntsel = ws;
963 }
964 }
965 }
966
967 return (ntsel != null && use_selected) ? ntsel : wayseg;
968 }
969
970 /**
971 * The *result* depends on the current map selection state IF use_selected is true.
972 *
973 * @return The nearest way segment to point p,
974 * and, depending on use_selected, prefers a selected way segment, if found.
975 * Also prefers segments of ways that are related to one of preferredRefs primitives
976 * @see #getNearestWaySegments(Point, Collection, Predicate)
977 * @since 6065
978 * @param p the point for which to search the nearest segment.
979 * @param predicate the returned object has to fulfill certain properties.
980 * @param use_selected whether selected way segments should be preferred.
981 * @param preferredRefs - prefer segments related to these primitives, may be null
982 */
983 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
984 boolean use_selected, Collection<OsmPrimitive> preferredRefs) {
985 WaySegment wayseg = null, ntsel = null, ntref = null;
986 if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
987
988 searchLoop: for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
989 for (WaySegment ws : wslist) {
990 if (wayseg == null) {
991 wayseg = ws;
992 }
993 if (ntsel == null && ws.way.isSelected()) {
994 ntsel = ws;
995 break searchLoop;
996 }
997 if (ntref == null && preferredRefs != null) {
998 // prefer ways containing given nodes
999 for (Node nd: ws.way.getNodes()) {
1000 if (preferredRefs.contains(nd)) {
1001 ntref = ws;
1002 break searchLoop;
1003 }
1004 }
1005 Collection<OsmPrimitive> wayRefs = ws.way.getReferrers();
1006 // prefer member of the given relations
1007 for (OsmPrimitive ref: preferredRefs) {
1008 if (ref instanceof Relation && wayRefs.contains(ref)) {
1009 ntref = ws;
1010 break searchLoop;
1011 }
1012 }
1013 }
1014 }
1015 }
1016 if (ntsel != null && use_selected)
1017 return ntsel;
1018 if (ntref != null)
1019 return ntref;
1020 return wayseg;
1021 }
1022
1023 /**
1024 * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
1025 *
1026 * @return The nearest way segment to point p.
1027 */
1028 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
1029 return getNearestWaySegment(p, predicate, true);
1030 }
1031
1032 /**
1033 * The *result* does not depend on the current map selection state,
1034 * neither does the result *order*.
1035 * It solely depends on the perpendicular distance to point p.
1036 *
1037 * @return all nearest ways to the screen point given that are not in ignore.
1038 * @see #getNearestWaySegments(Point, Collection, Predicate)
1039 *
1040 * @param p the point for which to search the nearest ways.
1041 * @param ignore a collection of ways which are not to be returned.
1042 * @param predicate the returned object has to fulfill certain properties.
1043 */
1044 public final List<Way> getNearestWays(Point p,
1045 Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
1046 List<Way> nearestList = new ArrayList<Way>();
1047 Set<Way> wset = new HashSet<Way>();
1048
1049 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1050 for (WaySegment ws : wss) {
1051 if (wset.add(ws.way)) {
1052 nearestList.add(ws.way);
1053 }
1054 }
1055 }
1056 if (ignore != null) {
1057 nearestList.removeAll(ignore);
1058 }
1059
1060 return nearestList;
1061 }
1062
1063 /**
1064 * The *result* does not depend on the current map selection state,
1065 * neither does the result *order*.
1066 * It solely depends on the perpendicular distance to point p.
1067 *
1068 * @return all nearest ways to the screen point given.
1069 * @see #getNearestWays(Point, Collection, Predicate)
1070 *
1071 * @param p the point for which to search the nearest ways.
1072 * @param predicate the returned object has to fulfill certain properties.
1073 */
1074 public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1075 return getNearestWays(p, null, predicate);
1076 }
1077
1078 /**
1079 * The *result* depends on the current map selection state.
1080 *
1081 * @return The nearest way to point p,
1082 * prefer a selected way if there are multiple nearest.
1083 * @see #getNearestWaySegment(Point, Predicate)
1084 *
1085 * @param p the point for which to search the nearest segment.
1086 * @param predicate the returned object has to fulfill certain properties.
1087 */
1088 public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
1089 WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
1090 return (nearestWaySeg == null) ? null : nearestWaySeg.way;
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 * First, nodes will be searched. If there are nodes within BBox found,
1099 * return a collection of those nodes only.
1100 *
1101 * If no nodes are found, search for nearest ways. If there are ways
1102 * within BBox found, return a collection of those ways only.
1103 *
1104 * If nothing is found, return an empty collection.
1105 *
1106 * @return Primitives nearest to the given screen point that are not in ignore.
1107 * @see #getNearestNodes(Point, Collection, Predicate)
1108 * @see #getNearestWays(Point, Collection, Predicate)
1109 *
1110 * @param p The point on screen.
1111 * @param ignore a collection of ways which are not to be returned.
1112 * @param predicate the returned object has to fulfill certain properties.
1113 */
1114 public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
1115 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1116 List<OsmPrimitive> nearestList = Collections.emptyList();
1117 OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
1118
1119 if (osm != null) {
1120 if (osm instanceof Node) {
1121 nearestList = new ArrayList<OsmPrimitive>(getNearestNodes(p, predicate));
1122 } else if (osm instanceof Way) {
1123 nearestList = new ArrayList<OsmPrimitive>(getNearestWays(p, predicate));
1124 }
1125 if (ignore != null) {
1126 nearestList.removeAll(ignore);
1127 }
1128 }
1129
1130 return nearestList;
1131 }
1132
1133 /**
1134 * The *result* does not depend on the current map selection state,
1135 * neither does the result *order*.
1136 * It solely depends on the distance to point p.
1137 *
1138 * @return Primitives nearest to the given screen point.
1139 * @see #getNearestNodesOrWays(Point, Collection, Predicate)
1140 *
1141 * @param p The point on screen.
1142 * @param predicate the returned object has to fulfill certain properties.
1143 */
1144 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1145 return getNearestNodesOrWays(p, null, predicate);
1146 }
1147
1148 /**
1149 * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
1150 * It decides, whether to yield the node to be tested or look for further (way) candidates.
1151 *
1152 * @return true, if the node fulfills the properties of the function body
1153 *
1154 * @param osm node to check
1155 * @param p point clicked
1156 * @param use_selected whether to prefer selected nodes
1157 */
1158 private boolean isPrecedenceNode(Node osm, Point p, boolean use_selected) {
1159 if (osm != null) {
1160 if (!(p.distanceSq(getPoint2D(osm)) > (4)*(4))) return true;
1161 if (osm.isTagged()) return true;
1162 if (use_selected && osm.isSelected()) return true;
1163 }
1164 return false;
1165 }
1166
1167 /**
1168 * The *result* depends on the current map selection state IF use_selected is true.
1169 *
1170 * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
1171 * the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)}
1172 * to find the nearest selected way.
1173 *
1174 * IF use_selected is false, or if no selected primitive was found, do the following.
1175 *
1176 * If the nearest node found is within 4px of p, simply take it.
1177 * Else, find the nearest way segment. Then, if p is closer to its
1178 * middle than to the node, take the way segment, else take the node.
1179 *
1180 * Finally, if no nearest primitive is found at all, return null.
1181 *
1182 * @return A primitive within snap-distance to point p,
1183 * that is chosen by the algorithm described.
1184 * @see #getNearestNode(Point, Predicate)
1185 * @see #getNearestNodesImpl(Point, Predicate)
1186 * @see #getNearestWay(Point, Predicate)
1187 *
1188 * @param p The point on screen.
1189 * @param predicate the returned object has to fulfill certain properties.
1190 * @param use_selected whether to prefer primitives that are currently selected or referred by selected primitives
1191 */
1192 public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) {
1193 Collection<OsmPrimitive> sel;
1194 DataSet ds = getCurrentDataSet();
1195 if (use_selected && ds!=null) {
1196 sel = ds.getSelected();
1197 } else {
1198 sel = null;
1199 }
1200 OsmPrimitive osm = getNearestNode(p, predicate, use_selected, sel);
1201
1202 if (isPrecedenceNode((Node)osm, p, use_selected)) return osm;
1203 WaySegment ws;
1204 if (use_selected) {
1205 ws = getNearestWaySegment(p, predicate, use_selected, sel);
1206 } else {
1207 ws = getNearestWaySegment(p, predicate, use_selected);
1208 }
1209 if (ws == null) return osm;
1210
1211 if ((ws.way.isSelected() && use_selected) || osm == null) {
1212 // either (no _selected_ nearest node found, if desired) or no nearest node was found
1213 osm = ws.way;
1214 } else {
1215 int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1216 maxWaySegLenSq *= maxWaySegLenSq;
1217
1218 Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
1219 Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
1220
1221 // is wayseg shorter than maxWaySegLenSq and
1222 // is p closer to the middle of wayseg than to the nearest node?
1223 if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1224 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node)osm))) {
1225 osm = ws.way;
1226 }
1227 }
1228 return osm;
1229 }
1230
1231 /**
1232 * @return o as collection of o's type.
1233 */
1234 public static <T> Collection<T> asColl(T o) {
1235 if (o == null)
1236 return Collections.emptySet();
1237 return Collections.singleton(o);
1238 }
1239
1240 public static double perDist(Point2D pt, Point2D a, Point2D b) {
1241 if (pt != null && a != null && b != null) {
1242 double pd = (
1243 (a.getX()-pt.getX())*(b.getX()-a.getX()) -
1244 (a.getY()-pt.getY())*(b.getY()-a.getY()) );
1245 return Math.abs(pd) / a.distance(b);
1246 }
1247 return 0d;
1248 }
1249
1250 /**
1251 *
1252 * @param pt point to project onto (ab)
1253 * @param a root of vector
1254 * @param b vector
1255 * @return point of intersection of line given by (ab)
1256 * with its orthogonal line running through pt
1257 */
1258 public static Point2D project(Point2D pt, Point2D a, Point2D b) {
1259 if (pt != null && a != null && b != null) {
1260 double r = ((
1261 (pt.getX()-a.getX())*(b.getX()-a.getX()) +
1262 (pt.getY()-a.getY())*(b.getY()-a.getY()) )
1263 / a.distanceSq(b));
1264 return project(r, a, b);
1265 }
1266 return null;
1267 }
1268
1269 /**
1270 * if r = 0 returns a, if r=1 returns b,
1271 * if r = 0.5 returns center between a and b, etc..
1272 *
1273 * @param r scale value
1274 * @param a root of vector
1275 * @param b vector
1276 * @return new point at a + r*(ab)
1277 */
1278 public static Point2D project(double r, Point2D a, Point2D b) {
1279 Point2D ret = null;
1280
1281 if (a != null && b != null) {
1282 ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1283 a.getY() + r*(b.getY()-a.getY()));
1284 }
1285 return ret;
1286 }
1287
1288 /**
1289 * The *result* does not depend on the current map selection state,
1290 * neither does the result *order*.
1291 * It solely depends on the distance to point p.
1292 *
1293 * @return a list of all objects that are nearest to point p and
1294 * not in ignore or an empty list if nothing was found.
1295 *
1296 * @param p The point on screen.
1297 * @param ignore a collection of ways which are not to be returned.
1298 * @param predicate the returned object has to fulfill certain properties.
1299 */
1300 public final List<OsmPrimitive> getAllNearest(Point p,
1301 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1302 List<OsmPrimitive> nearestList = new ArrayList<OsmPrimitive>();
1303 Set<Way> wset = new HashSet<Way>();
1304
1305 // add nearby ways
1306 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1307 for (WaySegment ws : wss) {
1308 if (wset.add(ws.way)) {
1309 nearestList.add(ws.way);
1310 }
1311 }
1312 }
1313
1314 // add nearby nodes
1315 for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
1316 nearestList.addAll(nlist);
1317 }
1318
1319 // add parent relations of nearby nodes and ways
1320 Set<OsmPrimitive> parentRelations = new HashSet<OsmPrimitive>();
1321 for (OsmPrimitive o : nearestList) {
1322 for (OsmPrimitive r : o.getReferrers()) {
1323 if (r instanceof Relation && predicate.evaluate(r)) {
1324 parentRelations.add(r);
1325 }
1326 }
1327 }
1328 nearestList.addAll(parentRelations);
1329
1330 if (ignore != null) {
1331 nearestList.removeAll(ignore);
1332 }
1333
1334 return nearestList;
1335 }
1336
1337 /**
1338 * The *result* does not depend on the current map selection state,
1339 * neither does the result *order*.
1340 * It solely depends on the distance to point p.
1341 *
1342 * @return a list of all objects that are nearest to point p
1343 * or an empty list if nothing was found.
1344 * @see #getAllNearest(Point, Collection, Predicate)
1345 *
1346 * @param p The point on screen.
1347 * @param predicate the returned object has to fulfill certain properties.
1348 */
1349 public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1350 return getAllNearest(p, null, predicate);
1351 }
1352
1353 /**
1354 * @return The projection to be used in calculating stuff.
1355 */
1356 public Projection getProjection() {
1357 return Main.getProjection();
1358 }
1359
1360 @Override
1361 public String helpTopic() {
1362 String n = getClass().getName();
1363 return n.substring(n.lastIndexOf('.')+1);
1364 }
1365
1366 /**
1367 * Return a ID which is unique as long as viewport dimensions are the same
1368 */
1369 public int getViewID() {
1370 String x = center.east() + "_" + center.north() + "_" + scale + "_" +
1371 getWidth() + "_" + getHeight() + "_" + getProjection().toString();
1372 java.util.zip.CRC32 id = new java.util.zip.CRC32();
1373 id.update(x.getBytes());
1374 return (int)id.getValue();
1375 }
1376
1377 /**
1378 * Returns the current system of measurement.
1379 * @return The current system of measurement (metric system by default).
1380 * @since 3490
1381 */
1382 public static SystemOfMeasurement getSystemOfMeasurement() {
1383 SystemOfMeasurement som = SYSTEMS_OF_MEASUREMENT.get(ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get());
1384 if (som == null)
1385 return METRIC_SOM;
1386 return som;
1387 }
1388
1389 /**
1390 * Sets the current system of measurement.
1391 * @param somKey The system of measurement key. Must be defined in {@link NavigatableComponent#SYSTEMS_OF_MEASUREMENT}.
1392 * @since 6056
1393 * @throws IllegalArgumentException if {@code somKey} is not known
1394 */
1395 public static void setSystemOfMeasurement(String somKey) {
1396 if (!SYSTEMS_OF_MEASUREMENT.containsKey(somKey)) {
1397 throw new IllegalArgumentException("Invalid system of measurement: "+somKey);
1398 }
1399 String oldKey = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get();
1400 if (ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.put(somKey)) {
1401 fireSoMChanged(oldKey, somKey);
1402 }
1403 }
1404
1405 /**
1406 * A system of units used to express length and area measurements.
1407 * @since 3406
1408 */
1409 public static class SystemOfMeasurement {
1410
1411 /** First value, in meters, used to translate unit according to above formula. */
1412 public final double aValue;
1413 /** Second value, in meters, used to translate unit according to above formula. */
1414 public final double bValue;
1415 /** First unit used to format text. */
1416 public final String aName;
1417 /** Second unit used to format text. */
1418 public final String bName;
1419 /** Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}. Set to {@code -1} if not used.
1420 * @since 5870 */
1421 public final double areaCustomValue;
1422 /** Specific optional area unit. Set to {@code null} if not used.
1423 * @since 5870 */
1424 public final String areaCustomName;
1425
1426 /**
1427 * System of measurement. Currently covers only length (and area) units.
1428 *
1429 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
1430 * x_a == x_m / aValue
1431 *
1432 * @param aValue First value, in meters, used to translate unit according to above formula.
1433 * @param aName First unit used to format text.
1434 * @param bValue Second value, in meters, used to translate unit according to above formula.
1435 * @param bName Second unit used to format text.
1436 */
1437 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName) {
1438 this(aValue, aName, bValue, bName, -1, null);
1439 }
1440
1441 /**
1442 * System of measurement. Currently covers only length (and area) units.
1443 *
1444 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
1445 * x_a == x_m / aValue
1446 *
1447 * @param aValue First value, in meters, used to translate unit according to above formula.
1448 * @param aName First unit used to format text.
1449 * @param bValue Second value, in meters, used to translate unit according to above formula.
1450 * @param bName Second unit used to format text.
1451 * @param areaCustomValue Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}.
1452 * Set to {@code -1} if not used.
1453 * @param areaCustomName Specific optional area unit. Set to {@code null} if not used.
1454 *
1455 * @since 5870
1456 */
1457 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName, double areaCustomValue, String areaCustomName) {
1458 this.aValue = aValue;
1459 this.aName = aName;
1460 this.bValue = bValue;
1461 this.bName = bName;
1462 this.areaCustomValue = areaCustomValue;
1463 this.areaCustomName = areaCustomName;
1464 }
1465
1466 /**
1467 * Returns the text describing the given distance in this system of measurement.
1468 * @param dist The distance in metres
1469 * @return The text describing the given distance in this system of measurement.
1470 */
1471 public String getDistText(double dist) {
1472 return getDistText(dist, null, 0.01);
1473 }
1474
1475 /**
1476 * Returns the text describing the given distance in this system of measurement.
1477 * @param dist The distance in metres
1478 * @param format A {@link NumberFormat} to format the area value
1479 * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
1480 * @return The text describing the given distance in this system of measurement.
1481 * @since 6422
1482 */
1483 public String getDistText(final double dist, final NumberFormat format, final double threshold) {
1484 double a = dist / aValue;
1485 if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && a > bValue / aValue)
1486 return formatText(dist / bValue, bName, format);
1487 else if (a < threshold)
1488 return "< " + formatText(threshold, aName, format);
1489 else
1490 return formatText(a, aName, format);
1491 }
1492
1493 /**
1494 * Returns the text describing the given area in this system of measurement.
1495 * @param area The area in square metres
1496 * @return The text describing the given area in this system of measurement.
1497 * @since 5560
1498 */
1499 public String getAreaText(double area) {
1500 return getAreaText(area, null, 0.01);
1501 }
1502
1503 /**
1504 * Returns the text describing the given area in this system of measurement.
1505 * @param area The area in square metres
1506 * @param format A {@link NumberFormat} to format the area value
1507 * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
1508 * @return The text describing the given area in this system of measurement.
1509 * @since 6422
1510 */
1511 public String getAreaText(final double area, final NumberFormat format, final double threshold) {
1512 double a = area / (aValue*aValue);
1513 boolean lowerOnly = Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false);
1514 boolean customAreaOnly = Main.pref.getBoolean("system_of_measurement.use_only_custom_area_unit", false);
1515 if ((!lowerOnly && areaCustomValue > 0 && a > areaCustomValue / (aValue*aValue) && a < (bValue*bValue) / (aValue*aValue)) || customAreaOnly)
1516 return formatText(area / areaCustomValue, areaCustomName, format);
1517 else if (!lowerOnly && a >= (bValue*bValue) / (aValue*aValue))
1518 return formatText(area / (bValue * bValue), bName + "\u00b2", format);
1519 else if (a < threshold)
1520 return "< " + formatText(threshold, aName + "\u00b2", format);
1521 else
1522 return formatText(a, aName + "\u00b2", format);
1523 }
1524
1525 private static String formatText(double v, String unit, NumberFormat format) {
1526 if (format != null) {
1527 return format.format(v) + " " + unit;
1528 }
1529 return String.format(Locale.US, "%." + (v<9.999999 ? 2 : 1) + "f %s", v, unit);
1530 }
1531 }
1532
1533 /**
1534 * Metric system (international standard).
1535 * @since 3406
1536 */
1537 public static final SystemOfMeasurement METRIC_SOM = new SystemOfMeasurement(1, "m", 1000, "km", 10000, "ha");
1538
1539 /**
1540 * Chinese system.
1541 * @since 3406
1542 */
1543 public static final SystemOfMeasurement CHINESE_SOM = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */);
1544
1545 /**
1546 * Imperial system (British Commonwealth and former British Empire).
1547 * @since 3406
1548 */
1549 public static final SystemOfMeasurement IMPERIAL_SOM = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", 4046.86, "ac");
1550
1551 /**
1552 * Nautical mile system (navigation, polar exploration).
1553 * @since 5549
1554 */
1555 public static final SystemOfMeasurement NAUTICAL_MILE_SOM = new SystemOfMeasurement(185.2, "kbl", 1852, "NM");
1556
1557 /**
1558 * Known systems of measurement.
1559 * @since 3406
1560 */
1561 public static final Map<String, SystemOfMeasurement> SYSTEMS_OF_MEASUREMENT;
1562 static {
1563 SYSTEMS_OF_MEASUREMENT = new LinkedHashMap<String, SystemOfMeasurement>();
1564 SYSTEMS_OF_MEASUREMENT.put(marktr("Metric"), METRIC_SOM);
1565 SYSTEMS_OF_MEASUREMENT.put(marktr("Chinese"), CHINESE_SOM);
1566 SYSTEMS_OF_MEASUREMENT.put(marktr("Imperial"), IMPERIAL_SOM);
1567 SYSTEMS_OF_MEASUREMENT.put(marktr("Nautical Mile"), NAUTICAL_MILE_SOM);
1568 }
1569
1570 private static class CursorInfo {
1571 public Cursor cursor;
1572 public Object object;
1573 public CursorInfo(Cursor c, Object o) {
1574 cursor = c;
1575 object = o;
1576 }
1577 }
1578
1579 private LinkedList<CursorInfo> cursors = new LinkedList<CursorInfo>();
1580
1581 /**
1582 * Set new cursor.
1583 */
1584 public void setNewCursor(Cursor cursor, Object reference) {
1585 if (!cursors.isEmpty()) {
1586 CursorInfo l = cursors.getLast();
1587 if(l != null && l.cursor == cursor && l.object == reference)
1588 return;
1589 stripCursors(reference);
1590 }
1591 cursors.add(new CursorInfo(cursor, reference));
1592 setCursor(cursor);
1593 }
1594
1595 public void setNewCursor(int cursor, Object reference) {
1596 setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1597 }
1598
1599 /**
1600 * Remove the new cursor and reset to previous
1601 */
1602 public void resetCursor(Object reference) {
1603 if (cursors.isEmpty()) {
1604 setCursor(null);
1605 return;
1606 }
1607 CursorInfo l = cursors.getLast();
1608 stripCursors(reference);
1609 if (l != null && l.object == reference) {
1610 if (cursors.isEmpty()) {
1611 setCursor(null);
1612 } else {
1613 setCursor(cursors.getLast().cursor);
1614 }
1615 }
1616 }
1617
1618 private void stripCursors(Object reference) {
1619 LinkedList<CursorInfo> c = new LinkedList<CursorInfo>();
1620 for(CursorInfo i : cursors) {
1621 if(i.object != reference) {
1622 c.add(i);
1623 }
1624 }
1625 cursors = c;
1626 }
1627
1628 @Override
1629 public void paint(Graphics g) {
1630 synchronized (paintRequestLock) {
1631 if (paintRect != null) {
1632 Graphics g2 = g.create();
1633 g2.setColor(Utils.complement(PaintColors.getBackgroundColor()));
1634 g2.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
1635 g2.dispose();
1636 }
1637 if (paintPoly != null) {
1638 Graphics g2 = g.create();
1639 g2.setColor(Utils.complement(PaintColors.getBackgroundColor()));
1640 g2.drawPolyline(paintPoly.xpoints, paintPoly.ypoints, paintPoly.npoints);
1641 g2.dispose();
1642 }
1643 }
1644 super.paint(g);
1645 }
1646
1647 /**
1648 * Requests to paint the given {@code Rectangle}.
1649 * @param r The Rectangle to draw
1650 * @see #requestClearRect
1651 * @since 5500
1652 */
1653 public void requestPaintRect(Rectangle r) {
1654 if (r != null) {
1655 synchronized (paintRequestLock) {
1656 paintRect = r;
1657 }
1658 repaint();
1659 }
1660 }
1661
1662 /**
1663 * Requests to paint the given {@code Polygon} as a polyline (unclosed polygon).
1664 * @param p The Polygon to draw
1665 * @see #requestClearPoly
1666 * @since 5500
1667 */
1668 public void requestPaintPoly(Polygon p) {
1669 if (p != null) {
1670 synchronized (paintRequestLock) {
1671 paintPoly = p;
1672 }
1673 repaint();
1674 }
1675 }
1676
1677 /**
1678 * Requests to clear the rectangled previously drawn.
1679 * @see #requestPaintRect
1680 * @since 5500
1681 */
1682 public void requestClearRect() {
1683 synchronized (paintRequestLock) {
1684 paintRect = null;
1685 }
1686 repaint();
1687 }
1688
1689 /**
1690 * Requests to clear the polyline previously drawn.
1691 * @see #requestPaintPoly
1692 * @since 5500
1693 */
1694 public void requestClearPoly() {
1695 synchronized (paintRequestLock) {
1696 paintPoly = null;
1697 }
1698 repaint();
1699 }
1700}
Note: See TracBrowser for help on using the repository browser.