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

Last change on this file since 6084 was 6070, checked in by stoecker, 11 years ago

see #8853 remove tabs, trailing spaces, windows line ends, strange characters

  • Property svn:eol-style set to native
File size: 60.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.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 use_selected ? getCurrentDataSet().getSelected() : null;
1184 OsmPrimitive osm = getNearestNode(p, predicate, use_selected, sel);
1185
1186 if (isPrecedenceNode((Node)osm, p, use_selected)) return osm;
1187 WaySegment ws;
1188 if (use_selected) {
1189 ws = getNearestWaySegment(p, predicate, use_selected, sel);
1190 } else {
1191 ws = getNearestWaySegment(p, predicate, use_selected);
1192 }
1193 if (ws == null) return osm;
1194
1195 if ((ws.way.isSelected() && use_selected) || osm == null) {
1196 // either (no _selected_ nearest node found, if desired) or no nearest node was found
1197 osm = ws.way;
1198 } else {
1199 int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1200 maxWaySegLenSq *= maxWaySegLenSq;
1201
1202 Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
1203 Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
1204
1205 // is wayseg shorter than maxWaySegLenSq and
1206 // is p closer to the middle of wayseg than to the nearest node?
1207 if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1208 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node)osm))) {
1209 osm = ws.way;
1210 }
1211 }
1212 return osm;
1213 }
1214
1215 /**
1216 * @return o as collection of o's type.
1217 */
1218 public static <T> Collection<T> asColl(T o) {
1219 if (o == null)
1220 return Collections.emptySet();
1221 return Collections.singleton(o);
1222 }
1223
1224 public static double perDist(Point2D pt, Point2D a, Point2D b) {
1225 if (pt != null && a != null && b != null) {
1226 double pd = (
1227 (a.getX()-pt.getX())*(b.getX()-a.getX()) -
1228 (a.getY()-pt.getY())*(b.getY()-a.getY()) );
1229 return Math.abs(pd) / a.distance(b);
1230 }
1231 return 0d;
1232 }
1233
1234 /**
1235 *
1236 * @param pt point to project onto (ab)
1237 * @param a root of vector
1238 * @param b vector
1239 * @return point of intersection of line given by (ab)
1240 * with its orthogonal line running through pt
1241 */
1242 public static Point2D project(Point2D pt, Point2D a, Point2D b) {
1243 if (pt != null && a != null && b != null) {
1244 double r = ((
1245 (pt.getX()-a.getX())*(b.getX()-a.getX()) +
1246 (pt.getY()-a.getY())*(b.getY()-a.getY()) )
1247 / a.distanceSq(b));
1248 return project(r, a, b);
1249 }
1250 return null;
1251 }
1252
1253 /**
1254 * if r = 0 returns a, if r=1 returns b,
1255 * if r = 0.5 returns center between a and b, etc..
1256 *
1257 * @param r scale value
1258 * @param a root of vector
1259 * @param b vector
1260 * @return new point at a + r*(ab)
1261 */
1262 public static Point2D project(double r, Point2D a, Point2D b) {
1263 Point2D ret = null;
1264
1265 if (a != null && b != null) {
1266 ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1267 a.getY() + r*(b.getY()-a.getY()));
1268 }
1269 return ret;
1270 }
1271
1272 /**
1273 * The *result* does not depend on the current map selection state,
1274 * neither does the result *order*.
1275 * It solely depends on the distance to point p.
1276 *
1277 * @return a list of all objects that are nearest to point p and
1278 * not in ignore or an empty list if nothing was found.
1279 *
1280 * @param p The point on screen.
1281 * @param ignore a collection of ways which are not to be returned.
1282 * @param predicate the returned object has to fulfill certain properties.
1283 */
1284 public final List<OsmPrimitive> getAllNearest(Point p,
1285 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1286 List<OsmPrimitive> nearestList = new ArrayList<OsmPrimitive>();
1287 Set<Way> wset = new HashSet<Way>();
1288
1289 // add nearby ways
1290 for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1291 for (WaySegment ws : wss) {
1292 if (wset.add(ws.way)) {
1293 nearestList.add(ws.way);
1294 }
1295 }
1296 }
1297
1298 // add nearby nodes
1299 for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
1300 nearestList.addAll(nlist);
1301 }
1302
1303 // add parent relations of nearby nodes and ways
1304 Set<OsmPrimitive> parentRelations = new HashSet<OsmPrimitive>();
1305 for (OsmPrimitive o : nearestList) {
1306 for (OsmPrimitive r : o.getReferrers()) {
1307 if (r instanceof Relation && predicate.evaluate(r)) {
1308 parentRelations.add(r);
1309 }
1310 }
1311 }
1312 nearestList.addAll(parentRelations);
1313
1314 if (ignore != null) {
1315 nearestList.removeAll(ignore);
1316 }
1317
1318 return nearestList;
1319 }
1320
1321 /**
1322 * The *result* does not depend on the current map selection state,
1323 * neither does the result *order*.
1324 * It solely depends on the distance to point p.
1325 *
1326 * @return a list of all objects that are nearest to point p
1327 * or an empty list if nothing was found.
1328 * @see #getAllNearest(Point, Collection, Predicate)
1329 *
1330 * @param p The point on screen.
1331 * @param predicate the returned object has to fulfill certain properties.
1332 */
1333 public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1334 return getAllNearest(p, null, predicate);
1335 }
1336
1337 /**
1338 * @return The projection to be used in calculating stuff.
1339 */
1340 public Projection getProjection() {
1341 return Main.getProjection();
1342 }
1343
1344 @Override
1345 public String helpTopic() {
1346 String n = getClass().getName();
1347 return n.substring(n.lastIndexOf('.')+1);
1348 }
1349
1350 /**
1351 * Return a ID which is unique as long as viewport dimensions are the same
1352 */
1353 public int getViewID() {
1354 String x = center.east() + "_" + center.north() + "_" + scale + "_" +
1355 getWidth() + "_" + getHeight() + "_" + getProjection().toString();
1356 java.util.zip.CRC32 id = new java.util.zip.CRC32();
1357 id.update(x.getBytes());
1358 return (int)id.getValue();
1359 }
1360
1361 /**
1362 * Returns the current system of measurement.
1363 * @return The current system of measurement (metric system by default).
1364 * @since 3490
1365 */
1366 public static SystemOfMeasurement getSystemOfMeasurement() {
1367 SystemOfMeasurement som = SYSTEMS_OF_MEASUREMENT.get(ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get());
1368 if (som == null)
1369 return METRIC_SOM;
1370 return som;
1371 }
1372
1373 /**
1374 * Sets the current system of measurement.
1375 * @param somKey The system of measurement key. Must be defined in {@link NavigatableComponent#SYSTEMS_OF_MEASUREMENT}.
1376 * @since 6056
1377 * @throws IllegalArgumentException if {@code somKey} is not known
1378 */
1379 public static void setSystemOfMeasurement(String somKey) {
1380 if (!SYSTEMS_OF_MEASUREMENT.containsKey(somKey)) {
1381 throw new IllegalArgumentException("Invalid system of measurement: "+somKey);
1382 }
1383 String oldKey = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get();
1384 if (ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.put(somKey)) {
1385 fireSoMChanged(oldKey, somKey);
1386 }
1387 }
1388
1389 /**
1390 * A system of units used to express length and area measurements.
1391 * @since 3406
1392 */
1393 public static class SystemOfMeasurement {
1394
1395 /** First value, in meters, used to translate unit according to above formula. */
1396 public final double aValue;
1397 /** Second value, in meters, used to translate unit according to above formula. */
1398 public final double bValue;
1399 /** First unit used to format text. */
1400 public final String aName;
1401 /** Second unit used to format text. */
1402 public final String bName;
1403 /** Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}. Set to {@code -1} if not used.
1404 * @since 5870 */
1405 public final double areaCustomValue;
1406 /** Specific optional area unit. Set to {@code null} if not used.
1407 * @since 5870 */
1408 public final String areaCustomName;
1409
1410 /**
1411 * System of measurement. Currently covers only length (and area) units.
1412 *
1413 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
1414 * x_a == x_m / aValue
1415 *
1416 * @param aValue First value, in meters, used to translate unit according to above formula.
1417 * @param aName First unit used to format text.
1418 * @param bValue Second value, in meters, used to translate unit according to above formula.
1419 * @param bName Second unit used to format text.
1420 */
1421 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName) {
1422 this(aValue, aName, bValue, bName, -1, null);
1423 }
1424
1425 /**
1426 * System of measurement. Currently covers only length (and area) units.
1427 *
1428 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
1429 * x_a == x_m / aValue
1430 *
1431 * @param aValue First value, in meters, used to translate unit according to above formula.
1432 * @param aName First unit used to format text.
1433 * @param bValue Second value, in meters, used to translate unit according to above formula.
1434 * @param bName Second unit used to format text.
1435 * @param areaCustomValue Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}.
1436 * Set to {@code -1} if not used.
1437 * @param areaCustomName Specific optional area unit. Set to {@code null} if not used.
1438 *
1439 * @since 5870
1440 */
1441 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName, double areaCustomValue, String areaCustomName) {
1442 this.aValue = aValue;
1443 this.aName = aName;
1444 this.bValue = bValue;
1445 this.bName = bName;
1446 this.areaCustomValue = areaCustomValue;
1447 this.areaCustomName = areaCustomName;
1448 }
1449
1450 /**
1451 * Returns the text describing the given distance in this system of measurement.
1452 * @param dist The distance in metres
1453 * @return The text describing the given distance in this system of measurement.
1454 */
1455 public String getDistText(double dist) {
1456 double a = dist / aValue;
1457 if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && a > bValue / aValue)
1458 return formatText(dist / bValue, bName);
1459 else if (a < 0.01)
1460 return "< 0.01 " + aName;
1461 else
1462 return formatText(a, aName);
1463 }
1464
1465 /**
1466 * Returns the text describing the given area in this system of measurement.
1467 * @param area The area in square metres
1468 * @return The text describing the given area in this system of measurement.
1469 * @since 5560
1470 */
1471 public String getAreaText(double area) {
1472 double a = area / (aValue*aValue);
1473 boolean lowerOnly = Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false);
1474 boolean customAreaOnly = Main.pref.getBoolean("system_of_measurement.use_only_custom_area_unit", false);
1475 if ((!lowerOnly && areaCustomValue > 0 && a > areaCustomValue / (aValue*aValue) && a < (bValue*bValue) / (aValue*aValue)) || customAreaOnly)
1476 return formatText(area / areaCustomValue, areaCustomName);
1477 else if (!lowerOnly && a >= (bValue*bValue) / (aValue*aValue))
1478 return formatText(area / (bValue*bValue), bName+"\u00b2");
1479 else if (a < 0.01)
1480 return "< 0.01 " + aName+"\u00b2";
1481 else
1482 return formatText(a, aName+"\u00b2");
1483 }
1484
1485 private static String formatText(double v, String unit) {
1486 return String.format(Locale.US, "%." + (v<9.999999 ? 2 : 1) + "f %s", v, unit);
1487 }
1488 }
1489
1490 /**
1491 * Metric system (international standard).
1492 * @since 3406
1493 */
1494 public static final SystemOfMeasurement METRIC_SOM = new SystemOfMeasurement(1, "m", 1000, "km", 10000, "ha");
1495
1496 /**
1497 * Chinese system.
1498 * @since 3406
1499 */
1500 public static final SystemOfMeasurement CHINESE_SOM = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */);
1501
1502 /**
1503 * Imperial system (British Commonwealth and former British Empire).
1504 * @since 3406
1505 */
1506 public static final SystemOfMeasurement IMPERIAL_SOM = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", 4046.86, "ac");
1507
1508 /**
1509 * Nautical mile system (navigation, polar exploration).
1510 * @since 5549
1511 */
1512 public static final SystemOfMeasurement NAUTICAL_MILE_SOM = new SystemOfMeasurement(185.2, "kbl", 1852, "NM");
1513
1514 /**
1515 * Known systems of measurement.
1516 * @since 3406
1517 */
1518 public static final Map<String, SystemOfMeasurement> SYSTEMS_OF_MEASUREMENT;
1519 static {
1520 SYSTEMS_OF_MEASUREMENT = new LinkedHashMap<String, SystemOfMeasurement>();
1521 SYSTEMS_OF_MEASUREMENT.put(marktr("Metric"), METRIC_SOM);
1522 SYSTEMS_OF_MEASUREMENT.put(marktr("Chinese"), CHINESE_SOM);
1523 SYSTEMS_OF_MEASUREMENT.put(marktr("Imperial"), IMPERIAL_SOM);
1524 SYSTEMS_OF_MEASUREMENT.put(marktr("Nautical Mile"), NAUTICAL_MILE_SOM);
1525 }
1526
1527 private static class CursorInfo {
1528 public Cursor cursor;
1529 public Object object;
1530 public CursorInfo(Cursor c, Object o) {
1531 cursor = c;
1532 object = o;
1533 }
1534 }
1535
1536 private LinkedList<CursorInfo> Cursors = new LinkedList<CursorInfo>();
1537 /**
1538 * Set new cursor.
1539 */
1540 public void setNewCursor(Cursor cursor, Object reference) {
1541 if(Cursors.size() > 0) {
1542 CursorInfo l = Cursors.getLast();
1543 if(l != null && l.cursor == cursor && l.object == reference)
1544 return;
1545 stripCursors(reference);
1546 }
1547 Cursors.add(new CursorInfo(cursor, reference));
1548 setCursor(cursor);
1549 }
1550 public void setNewCursor(int cursor, Object reference) {
1551 setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1552 }
1553 /**
1554 * Remove the new cursor and reset to previous
1555 */
1556 public void resetCursor(Object reference) {
1557 if(Cursors.size() == 0) {
1558 setCursor(null);
1559 return;
1560 }
1561 CursorInfo l = Cursors.getLast();
1562 stripCursors(reference);
1563 if(l != null && l.object == reference) {
1564 if(Cursors.size() == 0) {
1565 setCursor(null);
1566 } else {
1567 setCursor(Cursors.getLast().cursor);
1568 }
1569 }
1570 }
1571
1572 private void stripCursors(Object reference) {
1573 LinkedList<CursorInfo> c = new LinkedList<CursorInfo>();
1574 for(CursorInfo i : Cursors) {
1575 if(i.object != reference) {
1576 c.add(i);
1577 }
1578 }
1579 Cursors = c;
1580 }
1581
1582 @Override
1583 public void paint(Graphics g) {
1584 synchronized (paintRequestLock) {
1585 if (paintRect != null) {
1586 Graphics g2 = g.create();
1587 g2.setColor(Utils.complement(PaintColors.getBackgroundColor()));
1588 g2.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
1589 g2.dispose();
1590 }
1591 if (paintPoly != null) {
1592 Graphics g2 = g.create();
1593 g2.setColor(Utils.complement(PaintColors.getBackgroundColor()));
1594 g2.drawPolyline(paintPoly.xpoints, paintPoly.ypoints, paintPoly.npoints);
1595 g2.dispose();
1596 }
1597 }
1598 super.paint(g);
1599 }
1600
1601 /**
1602 * Requests to paint the given {@code Rectangle}.
1603 * @param r The Rectangle to draw
1604 * @see #requestClearRect
1605 * @since 5500
1606 */
1607 public void requestPaintRect(Rectangle r) {
1608 if (r != null) {
1609 synchronized (paintRequestLock) {
1610 paintRect = r;
1611 }
1612 repaint();
1613 }
1614 }
1615
1616 /**
1617 * Requests to paint the given {@code Polygon} as a polyline (unclosed polygon).
1618 * @param p The Polygon to draw
1619 * @see #requestClearPoly
1620 * @since 5500
1621 */
1622 public void requestPaintPoly(Polygon p) {
1623 if (p != null) {
1624 synchronized (paintRequestLock) {
1625 paintPoly = p;
1626 }
1627 repaint();
1628 }
1629 }
1630
1631 /**
1632 * Requests to clear the rectangled previously drawn.
1633 * @see #requestPaintRect
1634 * @since 5500
1635 */
1636 public void requestClearRect() {
1637 synchronized (paintRequestLock) {
1638 paintRect = null;
1639 }
1640 repaint();
1641 }
1642
1643 /**
1644 * Requests to clear the polyline previously drawn.
1645 * @see #requestPaintPoly
1646 * @since 5500
1647 */
1648 public void requestClearPoly() {
1649 synchronized (paintRequestLock) {
1650 paintPoly = null;
1651 }
1652 repaint();
1653 }
1654}
Note: See TracBrowser for help on using the repository browser.