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

Last change on this file since 9997 was 9976, checked in by Don-vip, 8 years ago

sonar - code simplification

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