// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.zip.CRC32;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.ProjectionBounds;
import org.openstreetmap.josm.data.SystemOfMeasurement;
import org.openstreetmap.josm.data.ViewportData;
import org.openstreetmap.josm.data.coor.CachedLatLon;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WaySegment;
import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
import org.openstreetmap.josm.data.preferences.BooleanProperty;
import org.openstreetmap.josm.data.preferences.DoubleProperty;
import org.openstreetmap.josm.data.preferences.IntegerProperty;
import org.openstreetmap.josm.data.projection.Projection;
import org.openstreetmap.josm.data.projection.Projections;
import org.openstreetmap.josm.gui.help.Helpful;
import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
import org.openstreetmap.josm.gui.layer.NativeScaleLayer.Scale;
import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
import org.openstreetmap.josm.gui.util.CursorManager;
import org.openstreetmap.josm.tools.Utils;
/**
* A component that can be navigated by a {@link MapMover}. Used as map view and for the
* zoomer in the download dialog.
*
* @author imi
* @since 41
*/
public class NavigatableComponent extends JComponent implements Helpful {
/**
* Interface to notify listeners of the change of the zoom area.
* @since 10600 (functional interface)
*/
@FunctionalInterface
public interface ZoomChangeListener {
/**
* Method called when the zoom area has changed.
*/
void zoomChanged();
}
/**
* To determine if a primitive is currently selectable.
*/
public transient Predicate<OsmPrimitive> isSelectablePredicate = prim -> {
if (!prim.isSelectable()) return false;
// if it isn't displayed on screen, you cannot click on it
MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
try {
return !MapPaintStyles.getStyles().get(prim, getDist100Pixel(), this).isEmpty();
} finally {
MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
}
};
/** Snap distance */
public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
/** Zoom steps to get double scale */
public static final DoubleProperty PROP_ZOOM_RATIO = new DoubleProperty("zoom.ratio", 2.0);
/** Divide intervals between native resolution levels to smaller steps if they are much larger than zoom ratio */
public static final BooleanProperty PROP_ZOOM_INTERMEDIATE_STEPS = new BooleanProperty("zoom.intermediate-steps", true);
/** Property name for center change events */
public static final String PROPNAME_CENTER = "center";
/** Property name for scale change events */
public static final String PROPNAME_SCALE = "scale";
/**
* The layer which scale is set to.
*/
private transient NativeScaleLayer nativeScaleLayer;
/**
* the zoom listeners
*/
private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();
/**
* Removes a zoom change listener
*
* @param listener the listener. Ignored if null or already absent
*/
public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
zoomChangeListeners.remove(listener);
}
/**
* Adds a zoom change listener
*
* @param listener the listener. Ignored if null or already registered.
*/
public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
if (listener != null) {
zoomChangeListeners.addIfAbsent(listener);
}
}
protected static void fireZoomChanged() {
for (ZoomChangeListener l : zoomChangeListeners) {
l.zoomChanged();
}
}
// The only events that may move/resize this map view are window movements or changes to the map view size.
// We can clean this up more by only recalculating the state on repaint.
private final transient HierarchyListener hierarchyListener = e -> {
long interestingFlags = HierarchyEvent.ANCESTOR_MOVED | HierarchyEvent.SHOWING_CHANGED;
if ((e.getChangeFlags() & interestingFlags) != 0) {
updateLocationState();
}
};
private final transient ComponentAdapter componentListener = new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
updateLocationState();
}
@Override
public void componentResized(ComponentEvent e) {
updateLocationState();
}
};
protected transient ViewportData initialViewport;
protected final transient CursorManager cursorManager = new CursorManager(this);
/**
* The current state (scale, center, ...) of this map view.
*/
private transient MapViewState state;
/**
* Constructs a new {@code NavigatableComponent}.
*/
public NavigatableComponent() {
setLayout(null);
state = MapViewState.createDefaultState(getWidth(), getHeight());
// uses weak link.
Main.addProjectionChangeListener((oldValue, newValue) -> fixProjection());
}
@Override
public void addNotify() {
updateLocationState();
addHierarchyListener(hierarchyListener);
addComponentListener(componentListener);
super.addNotify();
}
@Override
public void removeNotify() {
removeHierarchyListener(hierarchyListener);
removeComponentListener(componentListener);
super.removeNotify();
}
/**
* Choose a layer that scale will be snap to its native scales.
* @param nativeScaleLayer layer to which scale will be snapped
*/
public void setNativeScaleLayer(NativeScaleLayer nativeScaleLayer) {
this.nativeScaleLayer = nativeScaleLayer;
zoomTo(getCenter(), scaleRound(getScale()));
repaint();
}
/**
* Replies the layer which scale is set to.
* @return the current scale layer (may be null)
*/
public NativeScaleLayer getNativeScaleLayer() {
return nativeScaleLayer;
}
/**
* Get a new scale that is zoomed in from previous scale
* and snapped to selected native scale layer.
* @return new scale
*/
public double scaleZoomIn() {
return scaleZoomManyTimes(-1);
}
/**
* Get a new scale that is zoomed out from previous scale
* and snapped to selected native scale layer.
* @return new scale
*/
public double scaleZoomOut() {
return scaleZoomManyTimes(1);
}
/**
* Get a new scale that is zoomed in/out a number of times
* from previous scale and snapped to selected native scale layer.
* @param times count of zoom operations, negative means zoom in
* @return new scale
*/
public double scaleZoomManyTimes(int times) {
if (nativeScaleLayer != null) {
ScaleList scaleList = nativeScaleLayer.getNativeScales();
if (scaleList != null) {
if (PROP_ZOOM_INTERMEDIATE_STEPS.get()) {
scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
}
Scale s = scaleList.scaleZoomTimes(getScale(), PROP_ZOOM_RATIO.get(), times);
return s != null ? s.getScale() : 0;
}
}
return getScale() * Math.pow(PROP_ZOOM_RATIO.get(), times);
}
/**
* Get a scale snapped to native resolutions, use round method.
* It gives nearest step from scale list.
* Use round method.
* @param scale to snap
* @return snapped scale
*/
public double scaleRound(double scale) {
return scaleSnap(scale, false);
}
/**
* Get a scale snapped to native resolutions.
* It gives nearest lower step from scale list, usable to fit objects.
* @param scale to snap
* @return snapped scale
*/
public double scaleFloor(double scale) {
return scaleSnap(scale, true);
}
/**
* Get a scale snapped to native resolutions.
* It gives nearest lower step from scale list, usable to fit objects.
* @param scale to snap
* @param floor use floor instead of round, set true when fitting view to objects
* @return new scale
*/
public double scaleSnap(double scale, boolean floor) {
if (nativeScaleLayer != null) {
ScaleList scaleList = nativeScaleLayer.getNativeScales();
if (scaleList != null) {
if (PROP_ZOOM_INTERMEDIATE_STEPS.get()) {
scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
}
Scale snapscale = scaleList.getSnapScale(scale, PROP_ZOOM_RATIO.get(), floor);
return snapscale != null ? snapscale.getScale() : scale;
}
}
return scale;
}
/**
* Zoom in current view. Use configured zoom step and scaling settings.
*/
public void zoomIn() {
zoomTo(state.getCenterAtPixel().getEastNorth(), scaleZoomIn());
}
/**
* Zoom out current view. Use configured zoom step and scaling settings.
*/
public void zoomOut() {
zoomTo(state.getCenterAtPixel().getEastNorth(), scaleZoomOut());
}
protected void updateLocationState() {
if (isVisibleOnScreen()) {
state = state.usingLocation(this);
}
}
protected boolean isVisibleOnScreen() {
return SwingUtilities.getWindowAncestor(this) != null && isShowing();
}
/**
* Changes the projection settings used for this map view.
* <p>
* Made public temporarily, will be made private later.
*/
public void fixProjection() {
state = state.usingProjection(Main.getProjection());
repaint();
}
/**
* Gets the current view state. This includes the scale, the current view area and the position.
* @return The current state.
*/
public MapViewState getState() {
return state;
}
/**
* Returns the text describing the given distance in the current system of measurement.
* @param dist The distance in metres.
* @return the text describing the given distance in the current system of measurement.
* @since 3406
*/
public static String getDistText(double dist) {
return SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist);
}
/**
* Returns the text describing the given distance in the current system of measurement.
* @param dist The distance in metres
* @param format A {@link NumberFormat} to format the area value
* @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
* @return the text describing the given distance in the current system of measurement.
* @since 7135
*/
public static String getDistText(final double dist, final NumberFormat format, final double threshold) {
return SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist, format, threshold);
}
/**
* Returns the text describing the distance in meter that correspond to 100 px on screen.
* @return the text describing the distance in meter that correspond to 100 px on screen
*/
public String getDist100PixelText() {
return getDistText(getDist100Pixel());
}
/**
* Get the distance in meter that correspond to 100 px on screen.
*
* @return the distance in meter that correspond to 100 px on screen
*/
public double getDist100Pixel() {
return getDist100Pixel(true);
}
/**
* Get the distance in meter that correspond to 100 px on screen.
*
* @param alwaysPositive if true, makes sure the return value is always
* > 0. (Two points 100 px apart can appear to be identical if the user
* has zoomed out a lot and the projection code does something funny.)
* @return the distance in meter that correspond to 100 px on screen
*/
public double getDist100Pixel(boolean alwaysPositive) {
int w = getWidth()/2;
int h = getHeight()/2;
LatLon ll1 = getLatLon(w-50, h);
LatLon ll2 = getLatLon(w+50, h);
double gcd = ll1.greatCircleDistance(ll2);
if (alwaysPositive && gcd <= 0)
return 0.1;
return gcd;
}
/**
* Returns the current center of the viewport.
*
* (Use {@link #zoomTo(EastNorth)} to the change the center.)
*
* @return the current center of the viewport
*/
public EastNorth getCenter() {
return state.getCenterAtPixel().getEastNorth();
}
/**
* Returns the current scale.
*
* In east/north units per pixel.
*
* @return the current scale
*/
public double getScale() {
return state.getScale();
}
/**
* @param x X-Pixelposition to get coordinate from
* @param y Y-Pixelposition to get coordinate from
*
* @return Geographic coordinates from a specific pixel coordination on the screen.
*/
public EastNorth getEastNorth(int x, int y) {
return state.getForView(x, y).getEastNorth();
}
/**
* Determines the projection bounds of view area.
* @return the projection bounds of view area
*/
public ProjectionBounds getProjectionBounds() {
return getState().getViewArea().getProjectionBounds();
}
/* FIXME: replace with better method - used by MapSlider */
public ProjectionBounds getMaxProjectionBounds() {
Bounds b = getProjection().getWorldBoundsLatLon();
return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
getProjection().latlon2eastNorth(b.getMax()));
}
/* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
public Bounds getRealBounds() {
return getState().getViewArea().getCornerBounds();
}
/**
* Returns unprojected geographic coordinates for a specific pixel position on the screen.
* @param x X-Pixelposition to get coordinate from
* @param y Y-Pixelposition to get coordinate from
*
* @return Geographic unprojected coordinates from a specific pixel position on the screen.
*/
public LatLon getLatLon(int x, int y) {
return getProjection().eastNorth2latlon(getEastNorth(x, y));
}
/**
* Returns unprojected geographic coordinates for a specific pixel position on the screen.
* @param x X-Pixelposition to get coordinate from
* @param y Y-Pixelposition to get coordinate from
*
* @return Geographic unprojected coordinates from a specific pixel position on the screen.
*/
public LatLon getLatLon(double x, double y) {
return getLatLon((int) x, (int) y);
}
/**
* Determines the projection bounds of given rectangle.
* @param r rectangle
* @return the projection bounds of {@code r}
*/
public ProjectionBounds getProjectionBounds(Rectangle r) {
return getState().getViewArea(r).getProjectionBounds();
}
/**
* @param r rectangle
* @return Minimum bounds that will cover rectangle
*/
public Bounds getLatLonBounds(Rectangle r) {
return Main.getProjection().getLatLonBoundsBox(getProjectionBounds(r));
}
/**
* Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
* @return The affine transform.
*/
public AffineTransform getAffineTransform() {
return getState().getAffineTransform();
}
/**
* Return the point on the screen where this Coordinate would be.
* @param p The point, where this geopoint would be drawn.
* @return The point on screen where "point" would be drawn, relative to the own top/left.
*/
public Point2D getPoint2D(EastNorth p) {
if (null == p)
return new Point();
return getState().getPointFor(p).getInView();
}
/**
* Return the point on the screen where this Coordinate would be.
* @param latlon The point, where this geopoint would be drawn.
* @return The point on screen where "point" would be drawn, relative to the own top/left.
*/
public Point2D getPoint2D(LatLon latlon) {
if (latlon == null)
return new Point();
else if (latlon instanceof CachedLatLon)
return getPoint2D(((CachedLatLon) latlon).getEastNorth());
else
return getPoint2D(getProjection().latlon2eastNorth(latlon));
}
/**
* Return the point on the screen where this Node would be.
* @param n The node, where this geopoint would be drawn.
* @return The point on screen where "node" would be drawn, relative to the own top/left.
*/
public Point2D getPoint2D(Node n) {
return getPoint2D(n.getEastNorth());
}
/**
* looses precision, may overflow (depends on p and current scale)
* @param p east/north
* @return point
* @see #getPoint2D(EastNorth)
*/
public Point getPoint(EastNorth p) {
Point2D d = getPoint2D(p);
return new Point((int) d.getX(), (int) d.getY());
}
/**
* looses precision, may overflow (depends on p and current scale)
* @param latlon lat/lon
* @return point
* @see #getPoint2D(LatLon)
*/
public Point getPoint(LatLon latlon) {
Point2D d = getPoint2D(latlon);
return new Point((int) d.getX(), (int) d.getY());
}
/**
* looses precision, may overflow (depends on p and current scale)
* @param n node
* @return point
* @see #getPoint2D(Node)
*/
public Point getPoint(Node n) {
Point2D d = getPoint2D(n);
return new Point((int) d.getX(), (int) d.getY());
}
/**
* Zoom to the given coordinate and scale.
*
* @param newCenter The center x-value (easting) to zoom to.
* @param newScale The scale to use.
*/
public void zoomTo(EastNorth newCenter, double newScale) {
zoomTo(newCenter, newScale, false);
}
/**
* Zoom to the given coordinate and scale.
*
* @param center The center x-value (easting) to zoom to.
* @param scale The scale to use.
* @param initial true if this call initializes the viewport.
*/
public void zoomTo(EastNorth center, double scale, boolean initial) {
Bounds b = getProjection().getWorldBoundsLatLon();
ProjectionBounds pb = getProjection().getWorldBoundsBoxEastNorth();
double newScale = scale;
int width = getWidth();
int height = getHeight();
// make sure, the center of the screen is within projection bounds
double east = center.east();
double north = center.north();
east = Math.max(east, pb.minEast);
east = Math.min(east, pb.maxEast);
north = Math.max(north, pb.minNorth);
north = Math.min(north, pb.maxNorth);
EastNorth newCenter = new EastNorth(east, north);
// don't zoom out too much, the world bounds should be at least
// half the size of the screen
double pbHeight = pb.maxNorth - pb.minNorth;
if (height > 0 && 2 * pbHeight < height * newScale) {
double newScaleH = 2 * pbHeight / height;
double pbWidth = pb.maxEast - pb.minEast;
if (width > 0 && 2 * pbWidth < width * newScale) {
double newScaleW = 2 * pbWidth / width;
newScale = Math.max(newScaleH, newScaleW);
}
}
// don't zoom in too much, minimum: 100 px = 1 cm
LatLon ll1 = getLatLon(width / 2 - 50, height / 2);
LatLon ll2 = getLatLon(width / 2 + 50, height / 2);
if (ll1.isValid() && ll2.isValid() && b.contains(ll1) && b.contains(ll2)) {
double dm = ll1.greatCircleDistance(ll2);
double den = 100 * getScale();
double scaleMin = 0.01 * den / dm / 100;
if (newScale < scaleMin && !Double.isInfinite(scaleMin)) {
newScale = scaleMin;
}
}
// snap scale to imagery if needed
newScale = scaleRound(newScale);
// Align to the pixel grid:
// This is a sub-pixel correction to ensure consistent drawing at a certain scale.
// For example take 2 nodes, that have a distance of exactly 2.6 pixels.
// Depending on the offset, the distance in rounded or truncated integer
// pixels will be 2 or 3. It is preferable to have a consistent distance
// and not switch back and forth as the viewport moves. This can be achieved by
// locking an arbitrary point to integer pixel coordinates. (Here the EastNorth
// origin is used as reference point.)
// Note that the normal right mouse button drag moves the map by integer pixel
// values, so it is not an issue in this case. It only shows when zooming
// in & back out, etc.
MapViewState mvs = getState().usingScale(newScale);
mvs = mvs.movedTo(mvs.getCenter(), newCenter);
Point2D enOrigin = mvs.getPointFor(new EastNorth(0, 0)).getInView();
// as a result of the alignment, it is common to round "half integer" values
// like 1.49999, which is numerically unstable; add small epsilon to resolve this
final double epsilon = 1e-3;
Point2D enOriginAligned = new Point2D.Double(
Math.round(enOrigin.getX()) + epsilon,
Math.round(enOrigin.getY()) + epsilon);
EastNorth enShift = mvs.getForView(enOriginAligned.getX(), enOriginAligned.getY()).getEastNorth();
newCenter = newCenter.subtract(enShift);
if (!newCenter.equals(getCenter()) || !Utils.equalsEpsilon(getScale(), newScale)) {
if (!initial) {
pushZoomUndo(getCenter(), getScale());
}
zoomNoUndoTo(newCenter, newScale, initial);
}
}
/**
* Zoom to the given coordinate without adding to the zoom undo buffer.
*
* @param newCenter The center x-value (easting) to zoom to.
* @param newScale The scale to use.
* @param initial true if this call initializes the viewport.
*/
private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) {
if (!Utils.equalsEpsilon(getScale(), newScale)) {
double oldScale = getScale();
state = state.usingScale(newScale);
if (!initial) {
firePropertyChange(PROPNAME_SCALE, oldScale, newScale);
}
}
if (!newCenter.equals(getCenter())) {
EastNorth oldCenter = getCenter();
state = state.movedTo(state.getCenter(), newCenter);
if (!initial) {
firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter);
}
}
if (!initial) {
repaint();
fireZoomChanged();
}
}
/**
* Zoom to given east/north.
* @param newCenter new center coordinates
*/
public void zoomTo(EastNorth newCenter) {
zoomTo(newCenter, getScale());
}
/**
* Zoom to given lat/lon.
* @param newCenter new center coordinates
*/
public void zoomTo(LatLon newCenter) {
zoomTo(Projections.project(newCenter));
}
/**
* Create a thread that moves the viewport to the given center in an animated fashion.
* @param newCenter new east/north center
*/
public void smoothScrollTo(EastNorth newCenter) {
// FIXME make these configurable.
final int fps = 20; // animation frames per second
final int speed = 1500; // milliseconds for full-screen-width pan
if (!newCenter.equals(getCenter())) {
final EastNorth oldCenter = getCenter();
final double distance = newCenter.distance(oldCenter) / getScale();
final double milliseconds = distance / getWidth() * speed;
final double frames = milliseconds * fps / 1000;
final EastNorth finalNewCenter = newCenter;
new Thread("smooth-scroller") {
@Override
public void run() {
for (int i = 0; i < frames; i++) {
// FIXME - not use zoom history here
zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames));
try {
Thread.sleep(1000L / fps);
} catch (InterruptedException ex) {
Main.warn("InterruptedException in "+NavigatableComponent.class.getSimpleName()+" during smooth scrolling");
Thread.currentThread().interrupt();
}
}
}
}.start();
}
}
public void zoomManyTimes(double x, double y, int times) {
double oldScale = getScale();
double newScale = scaleZoomManyTimes(times);
zoomToFactor(x, y, newScale / oldScale);
}
public void zoomToFactor(double x, double y, double factor) {
double newScale = getScale()*factor;
EastNorth oldUnderMouse = getState().getForView(x, y).getEastNorth();
MapViewState newState = getState().usingScale(newScale);
newState = newState.movedTo(newState.getForView(x, y), oldUnderMouse);
zoomTo(newState.getCenter().getEastNorth(), newScale);
}
public void zoomToFactor(EastNorth newCenter, double factor) {
zoomTo(newCenter, getScale()*factor);
}
public void zoomToFactor(double factor) {
zoomTo(getCenter(), getScale()*factor);
}
/**
* Zoom to given projection bounds.
* @param box new projection bounds
*/
public void zoomTo(ProjectionBounds box) {
// -20 to leave some border
int w = getWidth()-20;
if (w < 20) {
w = 20;
}
int h = getHeight()-20;
if (h < 20) {
h = 20;
}
double scaleX = (box.maxEast-box.minEast)/w;
double scaleY = (box.maxNorth-box.minNorth)/h;
double newScale = Math.max(scaleX, scaleY);
newScale = scaleFloor(newScale);
zoomTo(box.getCenter(), newScale);
}
/**
* Zoom to given bounds.
* @param box new bounds
*/
public void zoomTo(Bounds box) {
zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
getProjection().latlon2eastNorth(box.getMax())));
}
/**
* Zoom to given viewport data.
* @param viewport new viewport data
*/
public void zoomTo(ViewportData viewport) {
if (viewport == null) return;
if (viewport.getBounds() != null) {
BoundingXYVisitor box = new BoundingXYVisitor();
box.visit(viewport.getBounds());
zoomTo(box);
} else {
zoomTo(viewport.getCenter(), viewport.getScale(), true);
}
}
/**
* Set the new dimension to the view.
* @param box box to zoom to
*/
public void zoomTo(BoundingXYVisitor box) {
if (box == null) {
box = new BoundingXYVisitor();
}
if (box.getBounds() == null) {
box.visit(getProjection().getWorldBoundsLatLon());
}
if (!box.hasExtend()) {
box.enlargeBoundingBox();
}
zoomTo(box.getBounds());
}
private static class ZoomData {
private final EastNorth center;
private final double scale;
ZoomData(EastNorth center, double scale) {
this.center = center;
this.scale = scale;
}
public EastNorth getCenterEastNorth() {
return center;
}
public double getScale() {
return scale;
}
}
private final transient Stack<ZoomData> zoomUndoBuffer = new Stack<>();
private final transient Stack<ZoomData> zoomRedoBuffer = new Stack<>();
private Date zoomTimestamp = new Date();
private void pushZoomUndo(EastNorth center, double scale) {
Date now = new Date();
if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {
zoomUndoBuffer.push(new ZoomData(center, scale));
if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {
zoomUndoBuffer.remove(0);
}
zoomRedoBuffer.clear();
}
zoomTimestamp = now;
}
/**
* Zoom to previous location.
*/
public void zoomPrevious() {
if (!zoomUndoBuffer.isEmpty()) {
ZoomData zoom = zoomUndoBuffer.pop();
zoomRedoBuffer.push(new ZoomData(getCenter(), getScale()));
zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
}
}
/**
* Zoom to next location.
*/
public void zoomNext() {
if (!zoomRedoBuffer.isEmpty()) {
ZoomData zoom = zoomRedoBuffer.pop();
zoomUndoBuffer.push(new ZoomData(getCenter(), getScale()));
zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false);
}
}
/**
* Determines if zoom history contains "undo" entries.
* @return {@code true} if zoom history contains "undo" entries
*/
public boolean hasZoomUndoEntries() {
return !zoomUndoBuffer.isEmpty();
}
/**
* Determines if zoom history contains "redo" entries.
* @return {@code true} if zoom history contains "redo" entries
*/
public boolean hasZoomRedoEntries() {
return !zoomRedoBuffer.isEmpty();
}
private BBox getBBox(Point p, int snapDistance) {
return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
getLatLon(p.x + snapDistance, p.y + snapDistance));
}
/**
* The *result* does not depend on the current map selection state, neither does the result *order*.
* It solely depends on the distance to point p.
* @param p point
* @param predicate predicate to match
*
* @return a sorted map with the keys representing the distance of their associated nodes to point p.
*/
private Map<Double, List<Node>> getNearestNodesImpl(Point p, Predicate<OsmPrimitive> predicate) {
Map<Double, List<Node>> nearestMap = new TreeMap<>();
DataSet ds = Main.getLayerManager().getEditDataSet();
if (ds != null) {
double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
snapDistanceSq *= snapDistanceSq;
for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
if (predicate.test(n)
&& (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
List<Node> nlist;
if (nearestMap.containsKey(dist)) {
nlist = nearestMap.get(dist);
} else {
nlist = new LinkedList<>();
nearestMap.put(dist, nlist);
}
nlist.add(n);
}
}
}
return nearestMap;
}
/**
* The *result* does not depend on the current map selection state,
* neither does the result *order*.
* It solely depends on the distance to point p.
*
* @param p the point for which to search the nearest segment.
* @param ignore a collection of nodes which are not to be returned.
* @param predicate the returned objects have to fulfill certain properties.
*
* @return All nodes nearest to point p that are in a belt from
* dist(nearest) to dist(nearest)+4px around p and
* that are not in ignore.
*/
public final List<Node> getNearestNodes(Point p,
Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
List<Node> nearestList = Collections.emptyList();
if (ignore == null) {
ignore = Collections.emptySet();
}
Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
if (!nlists.isEmpty()) {
Double minDistSq = null;
for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
Double distSq = entry.getKey();
List<Node> nlist = entry.getValue();
// filter nodes to be ignored before determining minDistSq..
nlist.removeAll(ignore);
if (minDistSq == null) {
if (!nlist.isEmpty()) {
minDistSq = distSq;
nearestList = new ArrayList<>();
nearestList.addAll(nlist);
}
} else {
if (distSq-minDistSq < (4)*(4)) {
nearestList.addAll(nlist);
}
}
}
}
return nearestList;
}
/**
* The *result* does not depend on the current map selection state,
* neither does the result *order*.
* It solely depends on the distance to point p.
*
* @param p the point for which to search the nearest segment.
* @param predicate the returned objects have to fulfill certain properties.
*
* @return All nodes nearest to point p that are in a belt from
* dist(nearest) to dist(nearest)+4px around p.
* @see #getNearestNodes(Point, Collection, Predicate)
*/
public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
return getNearestNodes(p, null, predicate);
}
/**
* The *result* depends on the current map selection state IF use_selected is true.
*
* If more than one node within node.snap-distance pixels is found,
* the nearest node selected is returned IF use_selected is true.
*
* Else the nearest new/id=0 node within about the same distance
* as the true nearest node is returned.
*
* If no such node is found either, the true nearest node to p is returned.
*
* Finally, if a node is not found at all, null is returned.
*
* @param p the screen point
* @param predicate this parameter imposes a condition on the returned object, e.g.
* give the nearest node that is tagged.
* @param useSelected make search depend on selection
*
* @return A node within snap-distance to point p, that is chosen by the algorithm described.
*/
public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
return getNearestNode(p, predicate, useSelected, null);
}
/**
* The *result* depends on the current map selection state IF use_selected is true
*
* If more than one node within node.snap-distance pixels is found,
* the nearest node selected is returned IF use_selected is true.
*
* If there are no selected nodes near that point, the node that is related to some of the preferredRefs
*
* Else the nearest new/id=0 node within about the same distance
* as the true nearest node is returned.
*
* If no such node is found either, the true nearest node to p is returned.
*
* Finally, if a node is not found at all, null is returned.
*
* @param p the screen point
* @param predicate this parameter imposes a condition on the returned object, e.g.
* give the nearest node that is tagged.
* @param useSelected make search depend on selection
* @param preferredRefs primitives, whose nodes we prefer
*
* @return A node within snap-distance to point p, that is chosen by the algorithm described.
* @since 6065
*/
public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
if (nlists.isEmpty()) return null;
if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
Node ntsel = null, ntnew = null, ntref = null;
boolean useNtsel = useSelected;
double minDistSq = nlists.keySet().iterator().next();
for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
Double distSq = entry.getKey();
for (Node nd : entry.getValue()) {
// find the nearest selected node
if (ntsel == null && nd.isSelected()) {
ntsel = nd;
// if there are multiple nearest nodes, prefer the one
// that is selected. This is required in order to drag
// the selected node if multiple nodes have the same
// coordinates (e.g. after unglue)
useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
}
if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
List<OsmPrimitive> ndRefs = nd.getReferrers();
for (OsmPrimitive ref: preferredRefs) {
if (ndRefs.contains(ref)) {
ntref = nd;
break;
}
}
}
// find the nearest newest node that is within about the same
// distance as the true nearest node
if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
ntnew = nd;
}
}
}
// take nearest selected, nearest new or true nearest node to p, in that order
if (ntsel != null && useNtsel)
return ntsel;
if (ntref != null)
return ntref;
if (ntnew != null)
return ntnew;
return nlists.values().iterator().next().get(0);
}
/**
* Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
* @param p the screen point
* @param predicate this parameter imposes a condition on the returned object, e.g.
* give the nearest node that is tagged.
*
* @return The nearest node to point p.
*/
public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
return getNearestNode(p, predicate, true);
}
/**
* The *result* does not depend on the current map selection state, neither does the result *order*.
* It solely depends on the distance to point p.
* @param p the screen point
* @param predicate this parameter imposes a condition on the returned object, e.g.
* give the nearest node that is tagged.
*
* @return a sorted map with the keys representing the perpendicular
* distance of their associated way segments to point p.
*/
private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
DataSet ds = Main.getLayerManager().getEditDataSet();
if (ds != null) {
double snapDistanceSq = Main.pref.getInteger("mappaint.segment.snap-distance", 10);
snapDistanceSq *= snapDistanceSq;
for (Way w : ds.searchWays(getBBox(p, Main.pref.getInteger("mappaint.segment.snap-distance", 10)))) {
if (!predicate.test(w)) {
continue;
}
Node lastN = null;
int i = -2;
for (Node n : w.getNodes()) {
i++;
if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
continue;
}
if (lastN == null) {
lastN = n;
continue;
}
Point2D pA = getPoint2D(lastN);
Point2D pB = getPoint2D(n);
double c = pA.distanceSq(pB);
double a = p.distanceSq(pB);
double b = p.distanceSq(pA);
/* perpendicular distance squared
* loose some precision to account for possible deviations in the calculation above
* e.g. if identical (A and B) come about reversed in another way, values may differ
* -- zero out least significant 32 dual digits of mantissa..
*/
double perDistSq = Double.longBitsToDouble(
Double.doubleToLongBits(a - (a - b + c) * (a - b + c) / 4 / c)
>> 32 << 32); // resolution in numbers with large exponent not needed here..
if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
List<WaySegment> wslist;
if (nearestMap.containsKey(perDistSq)) {
wslist = nearestMap.get(perDistSq);
} else {
wslist = new LinkedList<>();
nearestMap.put(perDistSq, wslist);
}
wslist.add(new WaySegment(w, i));
}
lastN = n;
}
}
}
return nearestMap;
}
/**
* The result *order* depends on the current map selection state.
* Segments within 10px of p are searched and sorted by their distance to @param p,
* then, within groups of equally distant segments, prefer those that are selected.
*
* @param p the point for which to search the nearest segments.
* @param ignore a collection of segments which are not to be returned.
* @param predicate the returned objects have to fulfill certain properties.
*
* @return all segments within 10px of p that are not in ignore,
* sorted by their perpendicular distance.
*/
public final List<WaySegment> getNearestWaySegments(Point p,
Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
List<WaySegment> nearestList = new ArrayList<>();
List<WaySegment> unselected = new LinkedList<>();
for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
// put selected waysegs within each distance group first
// makes the order of nearestList dependent on current selection state
for (WaySegment ws : wss) {
(ws.way.isSelected() ? nearestList : unselected).add(ws);
}
nearestList.addAll(unselected);
unselected.clear();
}
if (ignore != null) {
nearestList.removeAll(ignore);
}
return nearestList;
}
/**
* The result *order* depends on the current map selection state.
*
* @param p the point for which to search the nearest segments.
* @param predicate the returned objects have to fulfill certain properties.
*
* @return all segments within 10px of p, sorted by their perpendicular distance.
* @see #getNearestWaySegments(Point, Collection, Predicate)
*/
public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
return getNearestWaySegments(p, null, predicate);
}
/**
* The *result* depends on the current map selection state IF use_selected is true.
*
* @param p the point for which to search the nearest segment.
* @param predicate the returned object has to fulfill certain properties.
* @param useSelected whether selected way segments should be preferred.
*
* @return The nearest way segment to point p,
* and, depending on use_selected, prefers a selected way segment, if found.
* @see #getNearestWaySegments(Point, Collection, Predicate)
*/
public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
WaySegment wayseg = null;
WaySegment ntsel = null;
for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
if (wayseg != null && ntsel != null) {
break;
}
for (WaySegment ws : wslist) {
if (wayseg == null) {
wayseg = ws;
}
if (ntsel == null && ws.way.isSelected()) {
ntsel = ws;
}
}
}
return (ntsel != null && useSelected) ? ntsel : wayseg;
}
/**
* The *result* depends on the current map selection state IF use_selected is true.
*
* @param p the point for which to search the nearest segment.
* @param predicate the returned object has to fulfill certain properties.
* @param useSelected whether selected way segments should be preferred.
* @param preferredRefs - prefer segments related to these primitives, may be null
*
* @return The nearest way segment to point p,
* and, depending on use_selected, prefers a selected way segment, if found.
* Also prefers segments of ways that are related to one of preferredRefs primitives
*
* @see #getNearestWaySegments(Point, Collection, Predicate)
* @since 6065
*/
public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
WaySegment wayseg = null;
WaySegment ntsel = null;
WaySegment ntref = null;
if (preferredRefs != null && preferredRefs.isEmpty())
preferredRefs = null;
searchLoop: for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
for (WaySegment ws : wslist) {
if (wayseg == null) {
wayseg = ws;
}
if (ntsel == null && ws.way.isSelected()) {
ntsel = ws;
break searchLoop;
}
if (ntref == null && preferredRefs != null) {
// prefer ways containing given nodes
for (Node nd: ws.way.getNodes()) {
if (preferredRefs.contains(nd)) {
ntref = ws;
break searchLoop;
}
}
Collection<OsmPrimitive> wayRefs = ws.way.getReferrers();
// prefer member of the given relations
for (OsmPrimitive ref: preferredRefs) {
if (ref instanceof Relation && wayRefs.contains(ref)) {
ntref = ws;
break searchLoop;
}
}
}
}
}
if (ntsel != null && useSelected)
return ntsel;
if (ntref != null)
return ntref;
return wayseg;
}
/**
* Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
* @param p the point for which to search the nearest segment.
* @param predicate the returned object has to fulfill certain properties.
*
* @return The nearest way segment to point p.
*/
public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
return getNearestWaySegment(p, predicate, true);
}
/**
* The *result* does not depend on the current map selection state,
* neither does the result *order*.
* It solely depends on the perpendicular distance to point p.
*
* @param p the point for which to search the nearest ways.
* @param ignore a collection of ways which are not to be returned.
* @param predicate the returned object has to fulfill certain properties.
*
* @return all nearest ways to the screen point given that are not in ignore.
* @see #getNearestWaySegments(Point, Collection, Predicate)
*/
public final List<Way> getNearestWays(Point p,
Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
List<Way> nearestList = new ArrayList<>();
Set<Way> wset = new HashSet<>();
for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
for (WaySegment ws : wss) {
if (wset.add(ws.way)) {
nearestList.add(ws.way);
}
}
}
if (ignore != null) {
nearestList.removeAll(ignore);
}
return nearestList;
}
/**
* The *result* does not depend on the current map selection state,
* neither does the result *order*.
* It solely depends on the perpendicular distance to point p.
*
* @param p the point for which to search the nearest ways.
* @param predicate the returned object has to fulfill certain properties.
*
* @return all nearest ways to the screen point given.
* @see #getNearestWays(Point, Collection, Predicate)
*/
public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
return getNearestWays(p, null, predicate);
}
/**
* The *result* depends on the current map selection state.
*
* @param p the point for which to search the nearest segment.
* @param predicate the returned object has to fulfill certain properties.
*
* @return The nearest way to point p, prefer a selected way if there are multiple nearest.
* @see #getNearestWaySegment(Point, Predicate)
*/
public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
return (nearestWaySeg == null) ? null : nearestWaySeg.way;
}
/**
* The *result* does not depend on the current map selection state,
* neither does the result *order*.
* It solely depends on the distance to point p.
*
* First, nodes will be searched. If there are nodes within BBox found,
* return a collection of those nodes only.
*
* If no nodes are found, search for nearest ways. If there are ways
* within BBox found, return a collection of those ways only.
*
* If nothing is found, return an empty collection.
*
* @param p The point on screen.
* @param ignore a collection of ways which are not to be returned.
* @param predicate the returned object has to fulfill certain properties.
*
* @return Primitives nearest to the given screen point that are not in ignore.
* @see #getNearestNodes(Point, Collection, Predicate)
* @see #getNearestWays(Point, Collection, Predicate)
*/
public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
List<OsmPrimitive> nearestList = Collections.emptyList();
OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
if (osm != null) {
if (osm instanceof Node) {
nearestList = new ArrayList<>(getNearestNodes(p, predicate));
} else if (osm instanceof Way) {
nearestList = new ArrayList<>(getNearestWays(p, predicate));
}
if (ignore != null) {
nearestList.removeAll(ignore);
}
}
return nearestList;
}
/**
* The *result* does not depend on the current map selection state,
* neither does the result *order*.
* It solely depends on the distance to point p.
*
* @param p The point on screen.
* @param predicate the returned object has to fulfill certain properties.
* @return Primitives nearest to the given screen point.
* @see #getNearestNodesOrWays(Point, Collection, Predicate)
*/
public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
return getNearestNodesOrWays(p, null, predicate);
}
/**
* This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
* It decides, whether to yield the node to be tested or look for further (way) candidates.
*
* @param osm node to check
* @param p point clicked
* @param useSelected whether to prefer selected nodes
* @return true, if the node fulfills the properties of the function body
*/
private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
if (osm != null) {
if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
if (osm.isTagged()) return true;
if (useSelected && osm.isSelected()) return true;
}
return false;
}
/**
* The *result* depends on the current map selection state IF use_selected is true.
*
* IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
* the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)}
* to find the nearest selected way.
*
* IF use_selected is false, or if no selected primitive was found, do the following.
*
* If the nearest node found is within 4px of p, simply take it.
* Else, find the nearest way segment. Then, if p is closer to its
* middle than to the node, take the way segment, else take the node.
*
* Finally, if no nearest primitive is found at all, return null.
*
* @param p The point on screen.
* @param predicate the returned object has to fulfill certain properties.
* @param useSelected whether to prefer primitives that are currently selected or referred by selected primitives
*
* @return A primitive within snap-distance to point p,
* that is chosen by the algorithm described.
* @see #getNearestNode(Point, Predicate)
* @see #getNearestWay(Point, Predicate)
*/
public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
Collection<OsmPrimitive> sel;
DataSet ds = Main.getLayerManager().getEditDataSet();
if (useSelected && ds != null) {
sel = ds.getSelected();
} else {
sel = null;
}
OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
WaySegment ws;
if (useSelected) {
ws = getNearestWaySegment(p, predicate, useSelected, sel);
} else {
ws = getNearestWaySegment(p, predicate, useSelected);
}
if (ws == null) return osm;
if ((ws.way.isSelected() && useSelected) || osm == null) {
// either (no _selected_ nearest node found, if desired) or no nearest node was found
osm = ws.way;
} else {
int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
maxWaySegLenSq *= maxWaySegLenSq;
Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
// is wayseg shorter than maxWaySegLenSq and
// is p closer to the middle of wayseg than to the nearest node?
if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
osm = ws.way;
}
}
return osm;
}
/**
* if r = 0 returns a, if r=1 returns b,
* if r = 0.5 returns center between a and b, etc..
*
* @param r scale value
* @param a root of vector
* @param b vector
* @return new point at a + r*(ab)
*/
public static Point2D project(double r, Point2D a, Point2D b) {
Point2D ret = null;
if (a != null && b != null) {
ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
a.getY() + r*(b.getY()-a.getY()));
}
return ret;
}
/**
* The *result* does not depend on the current map selection state, neither does the result *order*.
* It solely depends on the distance to point p.
*
* @param p The point on screen.
* @param ignore a collection of ways which are not to be returned.
* @param predicate the returned object has to fulfill certain properties.
*
* @return a list of all objects that are nearest to point p and
* not in ignore or an empty list if nothing was found.
*/
public final List<OsmPrimitive> getAllNearest(Point p,
Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
List<OsmPrimitive> nearestList = new ArrayList<>();
Set<Way> wset = new HashSet<>();
// add nearby ways
for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
for (WaySegment ws : wss) {
if (wset.add(ws.way)) {
nearestList.add(ws.way);
}
}
}
// add nearby nodes
for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
nearestList.addAll(nlist);
}
// add parent relations of nearby nodes and ways
Set<OsmPrimitive> parentRelations = new HashSet<>();
for (OsmPrimitive o : nearestList) {
for (OsmPrimitive r : o.getReferrers()) {
if (r instanceof Relation && predicate.test(r)) {
parentRelations.add(r);
}
}
}
nearestList.addAll(parentRelations);
if (ignore != null) {
nearestList.removeAll(ignore);
}
return nearestList;
}
/**
* The *result* does not depend on the current map selection state, neither does the result *order*.
* It solely depends on the distance to point p.
*
* @param p The point on screen.
* @param predicate the returned object has to fulfill certain properties.
*
* @return a list of all objects that are nearest to point p
* or an empty list if nothing was found.
* @see #getAllNearest(Point, Collection, Predicate)
*/
public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
return getAllNearest(p, null, predicate);
}
/**
* @return The projection to be used in calculating stuff.
*/
public Projection getProjection() {
return state.getProjection();
}
@Override
public String helpTopic() {
String n = getClass().getName();
return n.substring(n.lastIndexOf('.')+1);
}
/**
* Return a ID which is unique as long as viewport dimensions are the same
* @return A unique ID, as long as viewport dimensions are the same
*/
public int getViewID() {
EastNorth center = getCenter();
String x = new StringBuilder().append(center.east())
.append('_').append(center.north())
.append('_').append(getScale())
.append('_').append(getWidth())
.append('_').append(getHeight())
.append('_').append(getProjection()).toString();
CRC32 id = new CRC32();
id.update(x.getBytes(StandardCharsets.UTF_8));
return (int) id.getValue();
}
/**
* Set new cursor.
* @param cursor The new cursor to use.
* @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
*/
public void setNewCursor(Cursor cursor, Object reference) {
cursorManager.setNewCursor(cursor, reference);
}
/**
* Set new cursor.
* @param cursor the type of predefined cursor
* @param reference A reference object that can be passed to the next set/reset calls to identify the caller.
*/
public void setNewCursor(int cursor, Object reference) {
setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
}
/**
* Remove the new cursor and reset to previous
* @param reference Cursor reference
*/
public void resetCursor(Object reference) {
cursorManager.resetCursor(reference);
}
/**
* Gets the cursor manager that is used for this NavigatableComponent.
* @return The cursor manager.
*/
public CursorManager getCursorManager() {
return cursorManager;
}
/**
* Get a max scale for projection that describes world in 1/512 of the projection unit
* @return max scale
*/
public double getMaxScale() {
ProjectionBounds world = getMaxProjectionBounds();
return Math.max(
world.maxNorth-world.minNorth,
world.maxEast-world.minEast
)/512;
}
}