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

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

Fix NPE's when unsupported projection used with WMTS

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