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

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

see #12350 - fix coverity 1352420, 1352421, 1352422, 1352423, 1352424 (potential NPEs)

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