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

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

fix #13049 - Inform map view of Projection updates (patch by michael2402) - gsoc-core

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