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

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

fix #13306 - Make map paint code use double coordinates (patch by michael2402) - gsoc-core

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