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

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

remove deprecated stuff - gsoc-core

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