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

Last change on this file since 4775 was 4627, checked in by Don-vip, 12 years ago

Major performance improvements in multipolygons rendering

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