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

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

see #13001 - replace calls to Main.main.getCurrentDataSet() by Main.getLayerManager().getEditDataSet()

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