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

Last change on this file since 5016 was 5016, checked in by akks, 12 years ago

Patch by JoshDoe [Faster relation selection with Alt+click and middle-click], fix #7314, #7317

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