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

Last change on this file since 6109 was 6096, checked in by akks, 11 years ago

should fix #8927: NPE in select mode after delete active data layer

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