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

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

fix #13375 - Fix icon rendering (patch by michael2402) - gsoc-core + add unit test

  • Property svn:eol-style set to native
File size: 57.9 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[422]2package org.openstreetmap.josm.gui;
3
[3919]4import java.awt.Cursor;
[422]5import java.awt.Point;
[2450]6import java.awt.Rectangle;
[10375]7import java.awt.event.ComponentAdapter;
8import java.awt.event.ComponentEvent;
9import java.awt.event.HierarchyEvent;
10import java.awt.event.HierarchyListener;
[4627]11import java.awt.geom.AffineTransform;
[3594]12import java.awt.geom.Point2D;
[7082]13import java.nio.charset.StandardCharsets;
[7135]14import java.text.NumberFormat;
[1430]15import java.util.ArrayList;
[422]16import java.util.Collection;
[454]17import java.util.Collections;
[2758]18import java.util.Date;
[422]19import java.util.HashSet;
[1430]20import java.util.LinkedList;
21import java.util.List;
[3406]22import java.util.Map;
[6258]23import java.util.Map.Entry;
[3594]24import java.util.Set;
[8856]25import java.util.Stack;
[422]26import java.util.TreeMap;
[2759]27import java.util.concurrent.CopyOnWriteArrayList;
[10657]28import java.util.function.Predicate;
[6995]29import java.util.zip.CRC32;
[422]30
31import javax.swing.JComponent;
[10375]32import javax.swing.SwingUtilities;
[422]33
34import org.openstreetmap.josm.Main;
[1722]35import org.openstreetmap.josm.data.Bounds;
36import org.openstreetmap.josm.data.ProjectionBounds;
[6992]37import org.openstreetmap.josm.data.SystemOfMeasurement;
[7816]38import org.openstreetmap.josm.data.ViewportData;
[1725]39import org.openstreetmap.josm.data.coor.CachedLatLon;
[422]40import org.openstreetmap.josm.data.coor.EastNorth;
41import org.openstreetmap.josm.data.coor.LatLon;
[2450]42import org.openstreetmap.josm.data.osm.BBox;
[845]43import org.openstreetmap.josm.data.osm.DataSet;
[422]44import org.openstreetmap.josm.data.osm.Node;
45import org.openstreetmap.josm.data.osm.OsmPrimitive;
[5016]46import org.openstreetmap.josm.data.osm.Relation;
[422]47import org.openstreetmap.josm.data.osm.Way;
48import org.openstreetmap.josm.data.osm.WaySegment;
[7817]49import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
[9818]50import org.openstreetmap.josm.data.preferences.BooleanProperty;
51import org.openstreetmap.josm.data.preferences.DoubleProperty;
[3600]52import org.openstreetmap.josm.data.preferences.IntegerProperty;
[422]53import org.openstreetmap.josm.data.projection.Projection;
[4126]54import org.openstreetmap.josm.data.projection.Projections;
[2252]55import org.openstreetmap.josm.gui.help.Helpful;
[9818]56import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
57import org.openstreetmap.josm.gui.layer.NativeScaleLayer.Scale;
58import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
[7389]59import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
[7447]60import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
[8556]61import org.openstreetmap.josm.gui.util.CursorManager;
[5515]62import org.openstreetmap.josm.tools.Utils;
[422]63
64/**
[6992]65 * A component that can be navigated by a {@link MapMover}. Used as map view and for the
[422]66 * zoomer in the download dialog.
67 *
68 * @author imi
[6992]69 * @since 41
[422]70 */
71public class NavigatableComponent extends JComponent implements Helpful {
72
[2759]73 /**
74 * Interface to notify listeners of the change of the zoom area.
[10600]75 * @since 10600 (functional interface)
[2759]76 */
[10600]77 @FunctionalInterface
[2759]78 public interface ZoomChangeListener {
[6992]79 /**
80 * Method called when the zoom area has changed.
81 */
[2759]82 void zoomChanged();
83 }
[6070]84
[10611]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 {
[10634]90 return !MapPaintStyles.getStyles().get(prim, getDist100Pixel(), this).isEmpty();
[10611]91 } finally {
92 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
[7389]93 }
94 };
95
[3600]96 public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
[9818]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);
[3600]99
[4627]100 public static final String PROPNAME_CENTER = "center";
[10378]101 public static final String PROPNAME_SCALE = "scale";
[4869]102
[2759]103 /**
[9818]104 * The layer which scale is set to.
105 */
106 private transient NativeScaleLayer nativeScaleLayer;
107
108 /**
[2759]109 * the zoom listeners
110 */
[7005]111 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();
[2759]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
[10375]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.
[10611]141 private final transient HierarchyListener hierarchyListener = e -> {
142 long interestingFlags = HierarchyEvent.ANCESTOR_MOVED | HierarchyEvent.SHOWING_CHANGED;
143 if ((e.getChangeFlags() & interestingFlags) != 0) {
144 updateLocationState();
[10375]145 }
146 };
[422]147
[10409]148 private final transient ComponentAdapter componentListener = new ComponentAdapter() {
[10375]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
[8308]160 protected transient ViewportData initialViewport;
[7816]161
[8557]162 protected final transient CursorManager cursorManager = new CursorManager(this);
[8556]163
[6509]164 /**
[10375]165 * The current state (scale, center, ...) of this map view.
166 */
[10409]167 private transient MapViewState state;
[10375]168
169 /**
[6509]170 * Constructs a new {@code NavigatableComponent}.
171 */
[1169]172 public NavigatableComponent() {
173 setLayout(null);
[10375]174 state = MapViewState.createDefaultState(getWidth(), getHeight());
[10486]175 // uses weak link.
[10611]176 Main.addProjectionChangeListener((oldValue, newValue) -> fixProjection());
[1169]177 }
[422]178
[10375]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
[9818]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;
[10375]200 zoomTo(getCenter(), scaleRound(getScale()));
[9818]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();
[9946]239 if (scaleList != null) {
[9954]240 if (PROP_ZOOM_INTERMEDIATE_STEPS.get()) {
[9946]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;
[9818]245 }
246 }
[9946]247 return getScale() * Math.pow(PROP_ZOOM_RATIO.get(), times);
[9818]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();
[9946]281 if (scaleList != null) {
[10466]282 if (PROP_ZOOM_INTERMEDIATE_STEPS.get()) {
283 scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
284 }
[9946]285 Scale snapscale = scaleList.getSnapScale(scale, PROP_ZOOM_RATIO.get(), floor);
286 return snapscale != null ? snapscale.getScale() : scale;
287 }
[9818]288 }
[9946]289 return scale;
[9818]290 }
291
292 /**
293 * Zoom in current view. Use configured zoom step and scaling settings.
294 */
295 public void zoomIn() {
[10856]296 zoomTo(state.getCenterAtPixel().getEastNorth(), scaleZoomIn());
[9818]297 }
298
299 /**
300 * Zoom out current view. Use configured zoom step and scaling settings.
301 */
302 public void zoomOut() {
[10856]303 zoomTo(state.getCenterAtPixel().getEastNorth(), scaleZoomOut());
[9818]304 }
305
[10395]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
[1814]312 protected DataSet getCurrentDataSet() {
[10446]313 return Main.getLayerManager().getEditDataSet();
[1169]314 }
[845]315
[10375]316 protected void updateLocationState() {
[10405]317 if (isVisibleOnScreen()) {
[10375]318 state = state.usingLocation(this);
[6509]319 }
[2114]320 }
321
[10405]322 protected boolean isVisibleOnScreen() {
323 return SwingUtilities.getWindowAncestor(this) != null && isShowing();
324 }
325
[5560]326 /**
[10486]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 /**
[10375]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 /**
[5560]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 */
[3406]350 public static String getDistText(double dist) {
[8554]351 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist);
[3406]352 }
353
[5560]354 /**
[7135]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) {
[8554]363 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist, format, threshold);
[7135]364 }
365
366 /**
[9954]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 */
[6992]370 public String getDist100PixelText() {
[3406]371 return getDistText(getDist100Pixel());
[1908]372 }
373
[9123]374 /**
375 * Get the distance in meter that correspond to 100 px on screen.
[9243]376 *
[9123]377 * @return the distance in meter that correspond to 100 px on screen
378 */
[6992]379 public double getDist100Pixel() {
[9123]380 return getDist100Pixel(true);
381 }
382
383 /**
384 * Get the distance in meter that correspond to 100 px on screen.
[9243]385 *
[9123]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) {
[1823]392 int w = getWidth()/2;
393 int h = getHeight()/2;
[8510]394 LatLon ll1 = getLatLon(w-50, h);
395 LatLon ll2 = getLatLon(w+50, h);
[9123]396 double gcd = ll1.greatCircleDistance(ll2);
397 if (alwaysPositive && gcd <= 0)
398 return 0.1;
399 return gcd;
[1722]400 }
401
[1169]402 /**
[9076]403 * Returns the current center of the viewport.
[9078]404 *
[9076]405 * (Use {@link #zoomTo(EastNorth)} to the change the center.)
[9078]406 *
[9076]407 * @return the current center of the viewport
[1169]408 */
409 public EastNorth getCenter() {
[10856]410 return state.getCenterAtPixel().getEastNorth();
[1169]411 }
[422]412
[9076]413 /**
414 * Returns the current scale.
[9078]415 *
[9076]416 * In east/north units per pixel.
[9078]417 *
[9076]418 * @return the current scale
419 */
[5670]420 public double getScale() {
[10375]421 return state.getScale();
[5670]422 }
423
[1169]424 /**
425 * @param x X-Pixelposition to get coordinate from
426 * @param y Y-Pixelposition to get coordinate from
427 *
[6992]428 * @return Geographic coordinates from a specific pixel coordination on the screen.
[1169]429 */
430 public EastNorth getEastNorth(int x, int y) {
[10375]431 return state.getForView(x, y).getEastNorth();
[1169]432 }
[422]433
[1722]434 public ProjectionBounds getProjectionBounds() {
[10375]435 return getState().getViewArea().getProjectionBounds();
[2114]436 }
[1722]437
[1823]438 /* FIXME: replace with better method - used by MapSlider */
439 public ProjectionBounds getMaxProjectionBounds() {
440 Bounds b = getProjection().getWorldBoundsLatLon();
[2327]441 return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
442 getProjection().latlon2eastNorth(b.getMax()));
[2114]443 }
[1823]444
445 /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
[1722]446 public Bounds getRealBounds() {
[10375]447 return getState().getViewArea().getCornerBounds();
[2114]448 }
[1722]449
[1169]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 }
[422]460
[3594]461 public LatLon getLatLon(double x, double y) {
[8510]462 return getLatLon((int) x, (int) y);
[3594]463 }
464
[9419]465 public ProjectionBounds getProjectionBounds(Rectangle r) {
[10458]466 return getState().getViewArea(r).getProjectionBounds();
[9419]467 }
[9623]468
[1169]469 /**
[8470]470 * @param r rectangle
[2450]471 * @return Minimum bounds that will cover rectangle
472 */
473 public Bounds getLatLonBounds(Rectangle r) {
[9419]474 return Main.getProjection().getLatLonBoundsBox(getProjectionBounds(r));
[2450]475 }
476
[4627]477 public AffineTransform getAffineTransform() {
[10375]478 return getState().getAffineTransform();
[4627]479 }
[4869]480
[2450]481 /**
[1169]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 */
[3594]487 public Point2D getPoint2D(EastNorth p) {
[1797]488 if (null == p)
[1169]489 return new Point();
[10375]490 return getState().getPointFor(p).getInView();
[1169]491 }
[422]492
[3594]493 public Point2D getPoint2D(LatLon latlon) {
[1797]494 if (latlon == null)
[1725]495 return new Point();
[1797]496 else if (latlon instanceof CachedLatLon)
[8510]497 return getPoint2D(((CachedLatLon) latlon).getEastNorth());
[1725]498 else
[3594]499 return getPoint2D(getProjection().latlon2eastNorth(latlon));
[1725]500 }
[4126]501
[3594]502 public Point2D getPoint2D(Node n) {
503 return getPoint2D(n.getEastNorth());
504 }
505
[10827]506 /**
[10843]507 * looses precision, may overflow (depends on p and current scale)
508 * @see #getPoint2D(EastNorth)
[10827]509 */
[3594]510 public Point getPoint(EastNorth p) {
511 Point2D d = getPoint2D(p);
512 return new Point((int) d.getX(), (int) d.getY());
513 }
514
[10827]515 /**
[10843]516 * looses precision, may overflow (depends on p and current scale)
517 * @see #getPoint2D(LatLon)
[10827]518 */
[3594]519 public Point getPoint(LatLon latlon) {
520 Point2D d = getPoint2D(latlon);
521 return new Point((int) d.getX(), (int) d.getY());
522 }
523
[10827]524 /**
[10843]525 * looses precision, may overflow (depends on p and current scale)
526 * @see #getPoint2D(Node)
[10827]527 */
[1725]528 public Point getPoint(Node n) {
[3594]529 Point2D d = getPoint2D(n);
530 return new Point((int) d.getX(), (int) d.getY());
[1725]531 }
532
[1169]533 /**
[7816]534 * Zoom to the given coordinate and scale.
535 *
[1169]536 * @param newCenter The center x-value (easting) to zoom to.
[5903]537 * @param newScale The scale to use.
[1169]538 */
[4079]539 public void zoomTo(EastNorth newCenter, double newScale) {
[7816]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) {
[1823]551 Bounds b = getProjection().getWorldBoundsLatLon();
[9118]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 }
[8413]575 }
[9118]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);
[9405]580 if (ll1.isValid() && ll2.isValid() && b.contains(ll1) && b.contains(ll2)) {
[10001]581 double dm = ll1.greatCircleDistance(ll2);
[10375]582 double den = 100 * getScale();
[10001]583 double scaleMin = 0.01 * den / dm / 100;
[9122]584 if (!Double.isInfinite(scaleMin) && newScale < scaleMin) {
[9118]585 newScale = scaleMin;
[2025]586 }
[1823]587 }
[2758]588
[9818]589 // snap scale to imagery if needed
[10375]590 newScale = scaleRound(newScale);
[9818]591
[10375]592 if (!newCenter.equals(getCenter()) || !Utils.equalsEpsilon(getScale(), newScale)) {
[7816]593 if (!initial) {
[10375]594 pushZoomUndo(getCenter(), getScale());
[7816]595 }
596 zoomNoUndoTo(newCenter, newScale, initial);
[2758]597 }
598 }
599
600 /**
601 * Zoom to the given coordinate without adding to the zoom undo buffer.
[7816]602 *
[2758]603 * @param newCenter The center x-value (easting) to zoom to.
[5903]604 * @param newScale The scale to use.
[7816]605 * @param initial true if this call initializes the viewport.
[2758]606 */
[8093]607 private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) {
[10375]608 if (!newCenter.equals(getCenter())) {
609 EastNorth oldCenter = getCenter();
[10856]610 state = state.movedTo(state.getCenterAtPixel(), newCenter);
[8093]611 if (!initial) {
[7816]612 firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter);
613 }
[2758]614 }
[10375]615 if (!Utils.equalsEpsilon(getScale(), newScale)) {
616 double oldScale = getScale();
617 state = state.usingScale(newScale);
618 // temporary. Zoom logic needs to be moved.
[10856]619 state = state.movedTo(state.getCenterAtPixel(), newCenter);
[8093]620 if (!initial) {
[7816]621 firePropertyChange(PROPNAME_SCALE, oldScale, newScale);
622 }
[1722]623 }
[1823]624
[8093]625 if (!initial) {
[7816]626 repaint();
627 fireZoomChanged();
628 }
[1169]629 }
[422]630
[1722]631 public void zoomTo(EastNorth newCenter) {
[10375]632 zoomTo(newCenter, getScale());
[1722]633 }
634
[1725]635 public void zoomTo(LatLon newCenter) {
[4126]636 zoomTo(Projections.project(newCenter));
[1725]637 }
638
[3837]639 /**
[9243]640 * Create a thread that moves the viewport to the given center in an animated fashion.
641 * @param newCenter new east/north center
[3837]642 */
643 public void smoothScrollTo(EastNorth newCenter) {
[6310]644 // FIXME make these configurable.
[3837]645 final int fps = 20; // animation frames per second
646 final int speed = 1500; // milliseconds for full-screen-width pan
[10375]647 if (!newCenter.equals(getCenter())) {
648 final EastNorth oldCenter = getCenter();
649 final double distance = newCenter.distance(oldCenter) / getScale();
[3837]650 final double milliseconds = distance / getWidth() * speed;
651 final double frames = milliseconds * fps / 1000;
652 final EastNorth finalNewCenter = newCenter;
653
[8736]654 new Thread("smooth-scroller") {
[5311]655 @Override
656 public void run() {
[8510]657 for (int i = 0; i < frames; i++) {
[6310]658 // FIXME - not use zoom history here
[5311]659 zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames));
[6310]660 try {
[10181]661 Thread.sleep(1000L / fps);
[6310]662 } catch (InterruptedException ex) {
[6313]663 Main.warn("InterruptedException in "+NavigatableComponent.class.getSimpleName()+" during smooth scrolling");
[6310]664 }
[3837]665 }
[5311]666 }
667 }.start();
[3837]668 }
669 }
670
[9818]671 public void zoomManyTimes(double x, double y, int times) {
[10375]672 double oldScale = getScale();
[9818]673 double newScale = scaleZoomManyTimes(times);
674 zoomToFactor(x, y, newScale / oldScale);
675 }
676
[1722]677 public void zoomToFactor(double x, double y, double factor) {
[10375]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);
[1722]683 }
684
685 public void zoomToFactor(EastNorth newCenter, double factor) {
[10375]686 zoomTo(newCenter, getScale()*factor);
[1722]687 }
688
689 public void zoomToFactor(double factor) {
[10375]690 zoomTo(getCenter(), getScale()*factor);
[1722]691 }
692
693 public void zoomTo(ProjectionBounds box) {
694 // -20 to leave some border
695 int w = getWidth()-20;
[1814]696 if (w < 20) {
[1722]697 w = 20;
[1814]698 }
[1722]699 int h = getHeight()-20;
[1814]700 if (h < 20) {
[1722]701 h = 20;
[1814]702 }
[1722]703
[4065]704 double scaleX = (box.maxEast-box.minEast)/w;
705 double scaleY = (box.maxNorth-box.minNorth)/h;
[1722]706 double newScale = Math.max(scaleX, scaleY);
707
[9818]708 newScale = scaleFloor(newScale);
[1722]709 zoomTo(box.getCenter(), newScale);
710 }
711
712 public void zoomTo(Bounds box) {
[2327]713 zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
714 getProjection().latlon2eastNorth(box.getMax())));
[1722]715 }
716
[7817]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.
[9243]730 * @param box box to zoom to
[7817]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
[9634]746 private static class ZoomData {
[9408]747 private final EastNorth center;
[8285]748 private final double scale;
[2758]749
[8836]750 ZoomData(EastNorth center, double scale) {
[9408]751 this.center = center;
[2758]752 this.scale = scale;
753 }
754
755 public EastNorth getCenterEastNorth() {
[9408]756 return center;
[2758]757 }
758
759 public double getScale() {
760 return scale;
761 }
762 }
763
[9623]764 private final transient Stack<ZoomData> zoomUndoBuffer = new Stack<>();
765 private final transient Stack<ZoomData> zoomRedoBuffer = new Stack<>();
[2758]766 private Date zoomTimestamp = new Date();
767
768 private void pushZoomUndo(EastNorth center, double scale) {
769 Date now = new Date();
[2760]770 if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {
[2758]771 zoomUndoBuffer.push(new ZoomData(center, scale));
[2760]772 if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {
[2766]773 zoomUndoBuffer.remove(0);
[2760]774 }
[2758]775 zoomRedoBuffer.clear();
776 }
777 zoomTimestamp = now;
778 }
779
780 public void zoomPrevious() {
781 if (!zoomUndoBuffer.isEmpty()) {
782 ZoomData zoom = zoomUndoBuffer.pop();
[10375]783 zoomRedoBuffer.push(new ZoomData(getCenter(), getScale()));
[7816]784 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
[2758]785 }
786 }
787
788 public void zoomNext() {
789 if (!zoomRedoBuffer.isEmpty()) {
790 ZoomData zoom = zoomRedoBuffer.pop();
[10375]791 zoomUndoBuffer.push(new ZoomData(getCenter(), getScale()));
[7816]792 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
[2758]793 }
794 }
795
[2759]796 public boolean hasZoomUndoEntries() {
797 return !zoomUndoBuffer.isEmpty();
798 }
799
800 public boolean hasZoomRedoEntries() {
801 return !zoomRedoBuffer.isEmpty();
802 }
803
[3594]804 private BBox getBBox(Point p, int snapDistance) {
[2426]805 return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
806 getLatLon(p.x + snapDistance, p.y + snapDistance));
[2422]807 }
808
[3594]809 /**
[9243]810 * The *result* does not depend on the current map selection state, neither does the result *order*.
[3594]811 * It solely depends on the distance to point p.
[9243]812 * @param p point
813 * @param predicate predicate to match
[3600]814 *
[9243]815 * @return a sorted map with the keys representing the distance of their associated nodes to point p.
[3594]816 */
[9243]817 private Map<Double, List<Node>> getNearestNodesImpl(Point p, Predicate<OsmPrimitive> predicate) {
[8338]818 Map<Double, List<Node>> nearestMap = new TreeMap<>();
[10395]819 DataSet ds = Main.getLayerManager().getEditDataSet();
[3594]820
821 if (ds != null) {
[3600]822 double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
[3594]823 snapDistanceSq *= snapDistanceSq;
824
[3600]825 for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
[10657]826 if (predicate.test(n)
[8395]827 && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
[3594]828 List<Node> nlist;
829 if (nearestMap.containsKey(dist)) {
830 nlist = nearestMap.get(dist);
831 } else {
[7005]832 nlist = new LinkedList<>();
[3594]833 nearestMap.put(dist, nlist);
834 }
835 nlist.add(n);
836 }
837 }
838 }
839
840 return nearestMap;
[3177]841 }
842
[1169]843 /**
[3594]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.
[3600]847 *
[8419]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 *
[3594]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;
[6258]867 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
868 Double distSq = entry.getKey();
869 List<Node> nlist = entry.getValue();
[3594]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;
[7005]876 nearestList = new ArrayList<>();
[3594]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.
[3600]894 *
[8419]895 * @param p the point for which to search the nearest segment.
896 * @param predicate the returned objects have to fulfill certain properties.
897 *
[3594]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 /**
[3642]907 * The *result* depends on the current map selection state IF use_selected is true.
[3600]908 *
[3594]909 * If more than one node within node.snap-distance pixels is found,
[3642]910 * the nearest node selected is returned IF use_selected is true.
[3600]911 *
[3642]912 * Else the nearest new/id=0 node within about the same distance
913 * as the true nearest node is returned.
[3600]914 *
[9243]915 * If no such node is found either, the true nearest node to p is returned.
[3600]916 *
[3642]917 * Finally, if a node is not found at all, null is returned.
[3600]918 *
[3177]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.
[9243]922 * @param useSelected make search depend on selection
[8419]923 *
[9243]924 * @return A node within snap-distance to point p, that is chosen by the algorithm described.
[1169]925 */
[6992]926 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
927 return getNearestNode(p, predicate, useSelected, null);
[6065]928 }
[6070]929
[6065]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.
[6070]935 *
[6065]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 *
[9243]941 * If no such node is found either, the true nearest node to p is returned.
[6065]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.
[9243]948 * @param useSelected make search depend on selection
[6065]949 * @param preferredRefs primitives, whose nodes we prefer
[8459]950 *
[9243]951 * @return A node within snap-distance to point p, that is chosen by the algorithm described.
[8459]952 * @since 6065
[6065]953 */
954 public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
[6258]955 boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
[6070]956
[3594]957 Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
[6065]958 if (nlists.isEmpty()) return null;
[6070]959
[6065]960 if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
961 Node ntsel = null, ntnew = null, ntref = null;
[6258]962 boolean useNtsel = useSelected;
[6065]963 double minDistSq = nlists.keySet().iterator().next();
[3594]964
[6258]965 for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
966 Double distSq = entry.getKey();
967 for (Node nd : entry.getValue()) {
[6065]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)
[8384]975 useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
[6065]976 }
[8384]977 if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
[6065]978 List<OsmPrimitive> ndRefs = nd.getReferrers();
979 for (OsmPrimitive ref: preferredRefs) {
980 if (ndRefs.contains(ref)) {
981 ntref = nd;
982 break;
983 }
[3594]984 }
985 }
[6065]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 }
[1169]991 }
[6065]992 }
[3594]993
[6065]994 // take nearest selected, nearest new or true nearest node to p, in that order
[6070]995 if (ntsel != null && useNtsel)
[6065]996 return ntsel;
[6070]997 if (ntref != null)
[6065]998 return ntref;
[6070]999 if (ntnew != null)
[6065]1000 return ntnew;
1001 return nlists.values().iterator().next().get(0);
[1169]1002 }
[422]1003
[3642]1004 /**
1005 * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
[6992]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.
[3642]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
[1169]1016 /**
[9243]1017 * The *result* does not depend on the current map selection state, neither does the result *order*.
[3594]1018 * It solely depends on the distance to point p.
[9243]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.
[3600]1022 *
[3594]1023 * @return a sorted map with the keys representing the perpendicular
1024 * distance of their associated way segments to point p.
[1169]1025 */
[9243]1026 private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
[7005]1027 Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
[10395]1028 DataSet ds = Main.getLayerManager().getEditDataSet();
[2422]1029
[3594]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)))) {
[10657]1035 if (!predicate.test(w)) {
[1814]1036 continue;
1037 }
[3594]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 }
[422]1049
[10001]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);
[3594]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(
[8443]1062 Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c)
[3594]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 {
[7005]1070 wslist = new LinkedList<>();
[3594]1071 nearestMap.put(perDistSq, wslist);
1072 }
1073 wslist.add(new WaySegment(w, i));
[1814]1074 }
[3594]1075
1076 lastN = n;
[1169]1077 }
[3594]1078 }
1079 }
[422]1080
[3594]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.
[3600]1088 *
[3594]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.
[8419]1092 *
1093 * @return all segments within 10px of p that are not in ignore,
1094 * sorted by their perpendicular distance.
[3594]1095 */
1096 public final List<WaySegment> getNearestWaySegments(Point p,
1097 Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
[7005]1098 List<WaySegment> nearestList = new ArrayList<>();
1099 List<WaySegment> unselected = new LinkedList<>();
[3594]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);
[1169]1106 }
[3594]1107 nearestList.addAll(unselected);
1108 unselected.clear();
[1169]1109 }
[3594]1110 if (ignore != null) {
1111 nearestList.removeAll(ignore);
[1169]1112 }
[3594]1113
[1169]1114 return nearestList;
1115 }
[422]1116
[1169]1117 /**
[3594]1118 * The result *order* depends on the current map selection state.
[3600]1119 *
[8419]1120 * @param p the point for which to search the nearest segments.
1121 * @param predicate the returned objects have to fulfill certain properties.
1122 *
[3594]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 /**
[3642]1131 * The *result* depends on the current map selection state IF use_selected is true.
[3600]1132 *
[8459]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 *
[3594]1137 * @return The nearest way segment to point p,
[3642]1138 * and, depending on use_selected, prefers a selected way segment, if found.
[3594]1139 * @see #getNearestWaySegments(Point, Collection, Predicate)
[1169]1140 */
[6992]1141 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
[10234]1142 WaySegment wayseg = null;
1143 WaySegment ntsel = null;
[3594]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
[6992]1159 return (ntsel != null && useSelected) ? ntsel : wayseg;
[3594]1160 }
[6070]1161
[9073]1162 /**
[6065]1163 * The *result* depends on the current map selection state IF use_selected is true.
1164 *
[8459]1165 * @param p the point for which to search the nearest segment.
1166 * @param predicate the returned object has to fulfill certain properties.
[10001]1167 * @param useSelected whether selected way segments should be preferred.
[8459]1168 * @param preferredRefs - prefer segments related to these primitives, may be null
1169 *
[6065]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
[8459]1173 *
[6065]1174 * @see #getNearestWaySegments(Point, Collection, Predicate)
1175 * @since 6065
1176 */
1177 public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
[10234]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;
[6070]1184
[6065]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 }
[10001]1213 if (ntsel != null && useSelected)
[6065]1214 return ntsel;
1215 if (ntref != null)
1216 return ntref;
1217 return wayseg;
1218 }
[3594]1219
1220 /**
[3642]1221 * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
[6992]1222 * @param p the point for which to search the nearest segment.
1223 * @param predicate the returned object has to fulfill certain properties.
[3642]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 /**
[3594]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.
[3600]1235 *
[3594]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.
[8419]1239 *
1240 * @return all nearest ways to the screen point given that are not in ignore.
1241 * @see #getNearestWaySegments(Point, Collection, Predicate)
[3594]1242 */
1243 public final List<Way> getNearestWays(Point p,
1244 Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
[7005]1245 List<Way> nearestList = new ArrayList<>();
1246 Set<Way> wset = new HashSet<>();
[3594]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 }
[1814]1255 if (ignore != null) {
[3594]1256 nearestList.removeAll(ignore);
[1814]1257 }
[3594]1258
1259 return nearestList;
[1169]1260 }
[422]1261
[1169]1262 /**
[3594]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.
[3600]1266 *
[8419]1267 * @param p the point for which to search the nearest ways.
1268 * @param predicate the returned object has to fulfill certain properties.
1269 *
[3594]1270 * @return all nearest ways to the screen point given.
1271 * @see #getNearestWays(Point, Collection, Predicate)
[1169]1272 */
[3594]1273 public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
1274 return getNearestWays(p, null, predicate);
[1169]1275 }
[422]1276
[3594]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.
[8459]1282 *
1283 * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
1284 * @see #getNearestWaySegment(Point, Predicate)
[3594]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
[1169]1291 /**
[3594]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.
[3600]1295 *
[3594]1296 * First, nodes will be searched. If there are nodes within BBox found,
1297 * return a collection of those nodes only.
[3600]1298 *
[3594]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.
[3600]1301 *
[3594]1302 * If nothing is found, return an empty collection.
[3600]1303 *
[8419]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 *
[3594]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)
[1169]1311 */
[3594]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;
[1169]1329 }
[422]1330
[1169]1331 /**
[3594]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.
[3600]1335 *
[8419]1336 * @param p The point on screen.
1337 * @param predicate the returned object has to fulfill certain properties.
[3594]1338 * @return Primitives nearest to the given screen point.
[5903]1339 * @see #getNearestNodesOrWays(Point, Collection, Predicate)
[1169]1340 */
[3594]1341 public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
1342 return getNearestNodesOrWays(p, null, predicate);
[1169]1343 }
[454]1344
[1169]1345 /**
[3594]1346 * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
[3642]1347 * It decides, whether to yield the node to be tested or look for further (way) candidates.
[3600]1348 *
[3594]1349 * @param osm node to check
1350 * @param p point clicked
[9976]1351 * @param useSelected whether to prefer selected nodes
[8419]1352 * @return true, if the node fulfills the properties of the function body
[1169]1353 */
[9976]1354 private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
[3594]1355 if (osm != null) {
[9976]1356 if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
[6065]1357 if (osm.isTagged()) return true;
[9976]1358 if (useSelected && osm.isSelected()) return true;
[3594]1359 }
[6065]1360 return false;
[1169]1361 }
[422]1362
[1169]1363 /**
[3594]1364 * The *result* depends on the current map selection state IF use_selected is true.
[3600]1365 *
[3594]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.
[3600]1369 *
[3594]1370 * IF use_selected is false, or if no selected primitive was found, do the following.
[3600]1371 *
[3594]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.
[3600]1375 *
[3594]1376 * Finally, if no nearest primitive is found at all, return null.
[1169]1377 *
[8459]1378 * @param p The point on screen.
1379 * @param predicate the returned object has to fulfill certain properties.
[10001]1380 * @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
[8459]1381 *
[3594]1382 * @return A primitive within snap-distance to point p,
1383 * that is chosen by the algorithm described.
[5903]1384 * @see #getNearestNode(Point, Predicate)
1385 * @see #getNearestWay(Point, Predicate)
[1169]1386 */
[10001]1387 public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
[6096]1388 Collection<OsmPrimitive> sel;
[10395]1389 DataSet ds = Main.getLayerManager().getEditDataSet();
[10001]1390 if (useSelected && ds != null) {
[6096]1391 sel = ds.getSelected();
1392 } else {
1393 sel = null;
1394 }
[10001]1395 OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
[3594]1396
[10001]1397 if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
[6065]1398 WaySegment ws;
[10001]1399 if (useSelected) {
1400 ws = getNearestWaySegment(p, predicate, useSelected, sel);
[6065]1401 } else {
[10001]1402 ws = getNearestWaySegment(p, predicate, useSelected);
[6065]1403 }
1404 if (ws == null) return osm;
[3594]1405
[10001]1406 if ((ws.way.isSelected() && useSelected) || osm == null) {
[6065]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;
[3594]1412
[6065]1413 Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
1414 Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
[3594]1415
[6065]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 &&
[8510]1419 p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
[6065]1420 osm = ws.way;
[1169]1421 }
1422 }
[3594]1423 return osm;
[1169]1424 }
[422]1425
[1169]1426 /**
[3594]1427 * if r = 0 returns a, if r=1 returns b,
1428 * if r = 0.5 returns center between a and b, etc..
[3600]1429 *
[3594]1430 * @param r scale value
1431 * @param a root of vector
1432 * @param b vector
1433 * @return new point at a + r*(ab)
1434 */
[3652]1435 public static Point2D project(double r, Point2D a, Point2D b) {
[3594]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 /**
[8459]1446 * The *result* does not depend on the current map selection state, neither does the result *order*.
[3594]1447 * It solely depends on the distance to point p.
[3600]1448 *
[3594]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.
[8459]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.
[3594]1455 */
1456 public final List<OsmPrimitive> getAllNearest(Point p,
1457 Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
[7005]1458 List<OsmPrimitive> nearestList = new ArrayList<>();
1459 Set<Way> wset = new HashSet<>();
[3594]1460
[5016]1461 // add nearby ways
[3594]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 }
[6070]1469
[5016]1470 // add nearby nodes
[3594]1471 for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
1472 nearestList.addAll(nlist);
1473 }
[6070]1474
[5016]1475 // add parent relations of nearby nodes and ways
[7005]1476 Set<OsmPrimitive> parentRelations = new HashSet<>();
[5016]1477 for (OsmPrimitive o : nearestList) {
1478 for (OsmPrimitive r : o.getReferrers()) {
[10657]1479 if (r instanceof Relation && predicate.test(r)) {
[5016]1480 parentRelations.add(r);
1481 }
1482 }
1483 }
1484 nearestList.addAll(parentRelations);
[6070]1485
[1814]1486 if (ignore != null) {
[3594]1487 nearestList.removeAll(ignore);
[1814]1488 }
[3594]1489
1490 return nearestList;
[1169]1491 }
[422]1492
[1169]1493 /**
[8459]1494 * The *result* does not depend on the current map selection state, neither does the result *order*.
[3594]1495 * It solely depends on the distance to point p.
[3600]1496 *
[8459]1497 * @param p The point on screen.
1498 * @param predicate the returned object has to fulfill certain properties.
1499 *
[3594]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 /**
[1169]1509 * @return The projection to be used in calculating stuff.
1510 */
[1823]1511 public Projection getProjection() {
[10375]1512 return state.getProjection();
[1169]1513 }
[422]1514
[6065]1515 @Override
[1169]1516 public String helpTopic() {
1517 String n = getClass().getName();
1518 return n.substring(n.lastIndexOf('.')+1);
[422]1519 }
[3116]1520
1521 /**
1522 * Return a ID which is unique as long as viewport dimensions are the same
[6992]1523 * @return A unique ID, as long as viewport dimensions are the same
[3116]1524 */
1525 public int getViewID() {
[10458]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();
[6995]1533 CRC32 id = new CRC32();
[7082]1534 id.update(x.getBytes(StandardCharsets.UTF_8));
[8510]1535 return (int) id.getValue();
[3116]1536 }
[3406]1537
[3919]1538 /**
1539 * Set new cursor.
[9243]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.
[3919]1542 */
1543 public void setNewCursor(Cursor cursor, Object reference) {
[8556]1544 cursorManager.setNewCursor(cursor, reference);
[3919]1545 }
[7082]1546
[9243]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 */
[3919]1552 public void setNewCursor(int cursor, Object reference) {
1553 setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1554 }
[7082]1555
[3919]1556 /**
1557 * Remove the new cursor and reset to previous
[9243]1558 * @param reference Cursor reference
[3919]1559 */
1560 public void resetCursor(Object reference) {
[8556]1561 cursorManager.resetCursor(reference);
[3919]1562 }
1563
[8556]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;
[3919]1570 }
[6070]1571
[5500]1572 /**
[9947]1573 * Get a max scale for projection that describes world in 1/512 of the projection unit
[9818]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
[9947]1581 )/512;
[9818]1582 }
[422]1583}
Note: See TracBrowser for help on using the repository browser.