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

Last change on this file since 8394 was 8384, checked in by Don-vip, 9 years ago

squid:S1244 - Floating point numbers should not be tested for equality

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