// License: GPL. See LICENSE file for details. package org.openstreetmap.josm.gui; import java.awt.Point; import java.awt.Rectangle; 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.Stack; import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.swing.JComponent; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.ProjectionBounds; 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.Way; import org.openstreetmap.josm.data.osm.WaySegment; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.gui.help.Helpful; import org.openstreetmap.josm.tools.Predicate; /** * An component that can be navigated by a mapmover. Used as map view and for the * zoomer in the download dialog. * * @author imi */ public class NavigatableComponent extends JComponent implements Helpful { /** * Interface to notify listeners of the change of the zoom area. */ public interface ZoomChangeListener { void zoomChanged(); } /** * the zoom listeners */ private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>(); /** * 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(); } } public static final int snapDistance = Main.pref.getInteger("node.snap-distance", 10); public static final int snapDistanceSq = sqr(snapDistance); private static int sqr(int a) { return a*a;} /** * The scale factor in x or y-units per pixel. This means, if scale = 10, * every physical pixel on screen are 10 x or 10 y units in the * northing/easting space of the projection. */ private double scale = Main.proj.getDefaultZoomInPPD(); /** * Center n/e coordinate of the desired screen center. */ protected EastNorth center = calculateDefaultCenter(); public NavigatableComponent() { setLayout(null); } protected DataSet getCurrentDataSet() { return Main.main.getCurrentDataSet(); } private EastNorth calculateDefaultCenter() { Bounds b = Main.proj.getWorldBoundsLatLon(); double lat = (b.getMax().lat() + b.getMin().lat())/2; double lon = (b.getMax().lon() + b.getMin().lon())/2; return Main.proj.latlon2eastNorth(new LatLon(lat, lon)); } public String getDist100PixelText() { double dist = getDist100Pixel(); return dist >= 2000 ? Math.round(dist/100)/10 +" km" : (dist >= 1 ? Math.round(dist*10)/10 +" m" : "< 1 m"); } public double getDist100Pixel() { int w = getWidth()/2; int h = getHeight()/2; LatLon ll1 = getLatLon(w-50,h); LatLon ll2 = getLatLon(w+50,h); return ll1.greatCircleDistance(ll2); } /** * @return Returns the center point. A copy is returned, so users cannot * change the center by accessing the return value. Use zoomTo instead. */ public EastNorth getCenter() { return center; } /** * @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 new EastNorth( center.east() + (x - getWidth()/2.0)*scale, center.north() - (y - getHeight()/2.0)*scale); } public ProjectionBounds getProjectionBounds() { return new ProjectionBounds( new EastNorth( center.east() - getWidth()/2.0*scale, center.north() - getHeight()/2.0*scale), new EastNorth( center.east() + getWidth()/2.0*scale, center.north() + getHeight()/2.0*scale)); } /* 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 new Bounds( getProjection().eastNorth2latlon(new EastNorth( center.east() - getWidth()/2.0*scale, center.north() - getHeight()/2.0*scale)), getProjection().eastNorth2latlon(new EastNorth( center.east() + getWidth()/2.0*scale, center.north() + getHeight()/2.0*scale))); } /** * @param x X-Pixelposition to get coordinate from * @param y Y-Pixelposition to get coordinate from * * @return Geographic unprojected coordinates from a specific pixel coordination * on the screen. */ public LatLon getLatLon(int x, int y) { return getProjection().eastNorth2latlon(getEastNorth(x, y)); } /** * @param r * @return Minimum bounds that will cover rectangle */ public Bounds getLatLonBounds(Rectangle r) { // TODO Maybe this should be (optional) method of Projection implementation EastNorth p1 = getEastNorth(r.x, r.y); EastNorth p2 = getEastNorth(r.x + r.width, r.y + r.height); Bounds result = new Bounds(Main.proj.eastNorth2latlon(p1)); double eastMin = Math.min(p1.east(), p2.east()); double eastMax = Math.max(p1.east(), p2.east()); double northMin = Math.min(p1.north(), p2.north()); double northMax = Math.max(p1.north(), p2.north()); double deltaEast = (eastMax - eastMin) / 10; double deltaNorth = (northMax - northMin) / 10; for (int i=0; i < 10; i++) { result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin))); result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax))); result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth))); result.extend(Main.proj.eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth))); } return result; } /** * 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 Point getPoint(EastNorth p) { if (null == p) return new Point(); double x = (p.east()-center.east())/scale + getWidth()/2; double y = (center.north()-p.north())/scale + getHeight()/2; return new Point((int)x,(int)y); } public Point getPoint(LatLon latlon) { if (latlon == null) return new Point(); else if (latlon instanceof CachedLatLon) return getPoint(((CachedLatLon)latlon).getEastNorth()); else return getPoint(getProjection().latlon2eastNorth(latlon)); } public Point getPoint(Node n) { return getPoint(n.getEastNorth()); } /** * Zoom to the given coordinate. * @param newCenter The center x-value (easting) to zoom to. * @param scale The scale to use. */ private void zoomTo(EastNorth newCenter, double newScale) { Bounds b = getProjection().getWorldBoundsLatLon(); CachedLatLon cl = new CachedLatLon(newCenter); boolean changed = false; double lat = cl.lat(); double lon = cl.lon(); if(lat < b.getMin().lat()) {changed = true; lat = b.getMin().lat(); } else if(lat > b.getMax().lat()) {changed = true; lat = b.getMax().lat(); } if(lon < b.getMin().lon()) {changed = true; lon = b.getMin().lon(); } else if(lon > b.getMax().lon()) {changed = true; lon = b.getMax().lon(); } if(changed) { newCenter = new CachedLatLon(lat, lon).getEastNorth(); } int width = getWidth()/2; int height = getHeight()/2; LatLon l1 = new LatLon(b.getMin().lat(), lon); LatLon l2 = new LatLon(b.getMax().lat(), lon); EastNorth e1 = getProjection().latlon2eastNorth(l1); EastNorth e2 = getProjection().latlon2eastNorth(l2); double d = e2.north() - e1.north(); if(d < height*newScale) { double newScaleH = d/height; e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMin().lon())); e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMax().lon())); d = e2.east() - e1.east(); if(d < width*newScale) { newScale = Math.max(newScaleH, d/width); } } else { d = d/(l1.greatCircleDistance(l2)*height*10); if(newScale < d) { newScale = d; } } if (!newCenter.equals(center) || (scale != newScale)) { pushZoomUndo(center, scale); zoomNoUndoTo(newCenter, newScale); } } /** * Zoom to the given coordinate without adding to the zoom undo buffer. * @param newCenter The center x-value (easting) to zoom to. * @param scale The scale to use. */ private void zoomNoUndoTo(EastNorth newCenter, double newScale) { if (!newCenter.equals(center)) { EastNorth oldCenter = center; center = newCenter; firePropertyChange("center", oldCenter, newCenter); } if (scale != newScale) { double oldScale = scale; scale = newScale; firePropertyChange("scale", oldScale, newScale); } repaint(); fireZoomChanged(); } public void zoomTo(EastNorth newCenter) { zoomTo(newCenter, scale); } public void zoomTo(LatLon newCenter) { if(newCenter instanceof CachedLatLon) { zoomTo(((CachedLatLon)newCenter).getEastNorth(), scale); } else { zoomTo(getProjection().latlon2eastNorth(newCenter), scale); } } public void zoomToFactor(double x, double y, double factor) { double newScale = scale*factor; // New center position so that point under the mouse pointer stays the same place as it was before zooming // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom zoomTo(new EastNorth( center.east() - (x - getWidth()/2.0) * (newScale - scale), center.north() + (y - getHeight()/2.0) * (newScale - scale)), newScale); } public void zoomToFactor(EastNorth newCenter, double factor) { zoomTo(newCenter, scale*factor); } public void zoomToFactor(double factor) { zoomTo(center, scale*factor); } 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.max.east()-box.min.east())/w; double scaleY = (box.max.north()-box.min.north())/h; double newScale = Math.max(scaleX, scaleY); zoomTo(box.getCenter(), newScale); } public void zoomTo(Bounds box) { zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()), getProjection().latlon2eastNorth(box.getMax()))); } private class ZoomData { LatLon center; double scale; public ZoomData(EastNorth center, double scale) { this.center = new CachedLatLon(center); this.scale = scale; } public EastNorth getCenterEastNorth() { return getProjection().latlon2eastNorth(center); } public double getScale() { return scale; } } private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>(); private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>(); 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; } public void zoomPrevious() { if (!zoomUndoBuffer.isEmpty()) { ZoomData zoom = zoomUndoBuffer.pop(); zoomRedoBuffer.push(new ZoomData(center, scale)); zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale()); } } public void zoomNext() { if (!zoomRedoBuffer.isEmpty()) { ZoomData zoom = zoomRedoBuffer.pop(); zoomUndoBuffer.push(new ZoomData(center, scale)); zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale()); } } public boolean hasZoomUndoEntries() { return !zoomUndoBuffer.isEmpty(); } public boolean hasZoomRedoEntries() { return !zoomRedoBuffer.isEmpty(); } private BBox getSnapDistanceBBox(Point p) { return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance), getLatLon(p.x + snapDistance, p.y + snapDistance)); } @Deprecated public final Node getNearestNode(Point p) { return getNearestNode(p, OsmPrimitive.isUsablePredicate); } /** * Return the nearest node to the screen point given. * If more then one node within snapDistance pixel is found, * the nearest node 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. */ public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) { DataSet ds = getCurrentDataSet(); if (ds == null) return null; double minDistanceSq = snapDistanceSq; Node minPrimitive = null; for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) { if (! predicate.evaluate(n)) continue; Point sp = getPoint(n); double dist = p.distanceSq(sp); if (dist < minDistanceSq) { minDistanceSq = dist; minPrimitive = n; } // when multiple nodes on one point, prefer new or selected nodes else if (dist == minDistanceSq && minPrimitive != null && ((n.isNew() && ds.isSelected(n)) || (!ds.isSelected(minPrimitive) && (ds.isSelected(n) || n.isNew())))) { minPrimitive = n; } } return minPrimitive; } /** * @return all way segments within 10px of p, sorted by their * perpendicular distance. * * @param p the point for which to search the nearest segment. * @param predicate the returned objects have to fulfill certain properties. */ public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) { TreeMap<Double, List<WaySegment>> nearest = new TreeMap<Double, List<WaySegment>>(); DataSet ds = getCurrentDataSet(); if (ds == null) return null; for (Way w : ds.searchWays(getSnapDistanceBBox(p))) { if (!predicate.evaluate(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; } Point A = getPoint(lastN); Point B = getPoint(n); double c = A.distanceSq(B); double a = p.distanceSq(B); double b = p.distanceSq(A); double perDist = a - (a - b + c) * (a - b + c) / 4 / c; // perpendicular distance squared if (perDist < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) { if (ds.isSelected(w)) { perDist -= 0.00001; } List<WaySegment> l; if (nearest.containsKey(perDist)) { l = nearest.get(perDist); } else { l = new LinkedList<WaySegment>(); nearest.put(perDist, l); } l.add(new WaySegment(w, i)); } lastN = n; } } ArrayList<WaySegment> nearestList = new ArrayList<WaySegment>(); for (List<WaySegment> wss : nearest.values()) { nearestList.addAll(wss); } return nearestList; } /** * @return the nearest way segment to the screen point given that is not * in ignore. * * @param p the point for which to search the nearest segment. * @param ignore a collection of segments which are not to be returned. * @param predicate the returned object has to fulfill certain properties. * May be null. */ public final WaySegment getNearestWaySegment (Point p, Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) { List<WaySegment> nearest = getNearestWaySegments(p, predicate); if(nearest == null) return null; if (ignore != null) { nearest.removeAll(ignore); } return nearest.isEmpty() ? null : nearest.get(0); } /** * @return the nearest way segment to the screen point given. */ public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) { return getNearestWaySegment(p, null, predicate); } @Deprecated public final Way getNearestWay(Point p) { return getNearestWay(p, OsmPrimitive.isUsablePredicate); } /** * @return the nearest way to the screen point given. */ public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) { WaySegment nearestWaySeg = getNearestWaySegment(p, predicate); return nearestWaySeg == null ? null : nearestWaySeg.way; } /** * Return the object, that is nearest to the given screen point. * * First, a node will be searched. If a node within 10 pixel is found, the * nearest node is returned. * * If no node is found, search for near ways. * * If nothing is found, return <code>null</code>. * * @param p The point on screen. * @param predicate the returned object has to fulfill certain properties. * @return The primitive that is nearest to the point p. */ public OsmPrimitive getNearest(Point p, Predicate<OsmPrimitive> predicate) { OsmPrimitive osm = getNearestNode(p, predicate); if (osm == null) { osm = getNearestWay(p, predicate); } return osm; } /** * Returns a singleton of the nearest object, or else an empty collection. */ public Collection<OsmPrimitive> getNearestCollection(Point p, Predicate<OsmPrimitive> predicate) { OsmPrimitive osm = getNearest(p, predicate); if (osm == null) return Collections.emptySet(); return Collections.singleton(osm); } /** * @return A list of all objects that are nearest to * the mouse. * * @return A collection of all items or <code>null</code> * if no item under or near the point. The returned * list is never empty. */ public Collection<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) { Collection<OsmPrimitive> nearest = new HashSet<OsmPrimitive>(); DataSet ds = getCurrentDataSet(); if (ds == null) return null; for (Way w : ds.searchWays(getSnapDistanceBBox(p))) { if (!predicate.evaluate(w)) continue; Node lastN = null; for (Node n : w.getNodes()) { if (!predicate.evaluate(n)) continue; if (lastN == null) { lastN = n; continue; } Point A = getPoint(lastN); Point B = getPoint(n); double c = A.distanceSq(B); double a = p.distanceSq(B); double b = p.distanceSq(A); double perDist = a - (a - b + c) * (a - b + c) / 4 / c; // perpendicular distance squared if (perDist < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) { nearest.add(w); break; } lastN = n; } } for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) { if (n.isUsable() && getPoint(n).distanceSq(p) < snapDistanceSq) { nearest.add(n); } } return nearest.isEmpty() ? null : nearest; } /** * @return A list of all nodes that are nearest to * the mouse. * * @return A collection of all nodes or <code>null</code> * if no node under or near the point. The returned * list is never empty. */ public Collection<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) { Collection<Node> nearest = new HashSet<Node>(); DataSet ds = getCurrentDataSet(); if (ds == null) return null; for (Node n : ds.searchNodes(getSnapDistanceBBox(p))) { if (!predicate.evaluate(n)) continue; if (getPoint(n).distanceSq(p) < snapDistanceSq) { nearest.add(n); } } return nearest.isEmpty() ? null : nearest; } /** * @return the nearest nodes to the screen point given that is not * in ignore. * * @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. * May be null. */ public final Collection<Node> getNearestNodes(Point p, Collection<Node> ignore, Predicate<OsmPrimitive> predicate) { Collection<Node> nearest = getNearestNodes(p, predicate); if (nearest == null) return null; if (ignore != null) { nearest.removeAll(ignore); } return nearest.isEmpty() ? null : nearest; } /** * @return The projection to be used in calculating stuff. */ public Projection getProjection() { return Main.proj; } 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 */ public int getViewID() { String x = center.east() + "_" + center.north() + "_" + scale + "_" + getWidth() + "_" + getHeight() + "_" + getProjection().toString(); java.util.zip.CRC32 id = new java.util.zip.CRC32(); id.update(x.getBytes()); return (int)id.getValue(); } }