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

Last change on this file since 9827 was 9825, checked in by wiktorn, 8 years ago

Improvements for native scales.

  • make ScaleList immutable object
  • reduce number of ScaleList object creation
  • use ScaleList.snapZoom for getBestZoom in WMTS

See: #12350

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