| 1 | // License: GPL. See LICENSE file for details. |
|---|
| 2 | package org.openstreetmap.josm.gui; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.marktr; |
|---|
| 5 | |
|---|
| 6 | import java.awt.Cursor; |
|---|
| 7 | import java.awt.Point; |
|---|
| 8 | import java.awt.Rectangle; |
|---|
| 9 | import java.awt.geom.AffineTransform; |
|---|
| 10 | import java.awt.geom.Point2D; |
|---|
| 11 | import java.util.ArrayList; |
|---|
| 12 | import java.util.Collection; |
|---|
| 13 | import java.util.Collections; |
|---|
| 14 | import java.util.Date; |
|---|
| 15 | import java.util.HashSet; |
|---|
| 16 | import java.util.LinkedHashMap; |
|---|
| 17 | import java.util.LinkedList; |
|---|
| 18 | import java.util.List; |
|---|
| 19 | import java.util.Locale; |
|---|
| 20 | import java.util.Map; |
|---|
| 21 | import java.util.Set; |
|---|
| 22 | import java.util.Stack; |
|---|
| 23 | import java.util.TreeMap; |
|---|
| 24 | import java.util.concurrent.CopyOnWriteArrayList; |
|---|
| 25 | |
|---|
| 26 | import javax.swing.JComponent; |
|---|
| 27 | |
|---|
| 28 | import org.openstreetmap.josm.Main; |
|---|
| 29 | import org.openstreetmap.josm.data.Bounds; |
|---|
| 30 | import org.openstreetmap.josm.data.ProjectionBounds; |
|---|
| 31 | import org.openstreetmap.josm.data.coor.CachedLatLon; |
|---|
| 32 | import org.openstreetmap.josm.data.coor.EastNorth; |
|---|
| 33 | import org.openstreetmap.josm.data.coor.LatLon; |
|---|
| 34 | import org.openstreetmap.josm.data.osm.BBox; |
|---|
| 35 | import org.openstreetmap.josm.data.osm.DataSet; |
|---|
| 36 | import org.openstreetmap.josm.data.osm.Node; |
|---|
| 37 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 38 | import org.openstreetmap.josm.data.osm.Relation; |
|---|
| 39 | import org.openstreetmap.josm.data.osm.Way; |
|---|
| 40 | import org.openstreetmap.josm.data.osm.WaySegment; |
|---|
| 41 | import org.openstreetmap.josm.data.preferences.IntegerProperty; |
|---|
| 42 | import org.openstreetmap.josm.data.projection.Projection; |
|---|
| 43 | import org.openstreetmap.josm.data.projection.Projections; |
|---|
| 44 | import org.openstreetmap.josm.gui.help.Helpful; |
|---|
| 45 | import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; |
|---|
| 46 | import org.openstreetmap.josm.tools.Predicate; |
|---|
| 47 | |
|---|
| 48 | /** |
|---|
| 49 | * An component that can be navigated by a mapmover. Used as map view and for the |
|---|
| 50 | * zoomer in the download dialog. |
|---|
| 51 | * |
|---|
| 52 | * @author imi |
|---|
| 53 | */ |
|---|
| 54 | public class NavigatableComponent extends JComponent implements Helpful { |
|---|
| 55 | |
|---|
| 56 | /** |
|---|
| 57 | * Interface to notify listeners of the change of the zoom area. |
|---|
| 58 | */ |
|---|
| 59 | public interface ZoomChangeListener { |
|---|
| 60 | void zoomChanged(); |
|---|
| 61 | } |
|---|
| 62 | |
|---|
| 63 | public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10); |
|---|
| 64 | |
|---|
| 65 | public static final String PROPNAME_CENTER = "center"; |
|---|
| 66 | public static final String PROPNAME_SCALE = "scale"; |
|---|
| 67 | |
|---|
| 68 | /** |
|---|
| 69 | * the zoom listeners |
|---|
| 70 | */ |
|---|
| 71 | private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>(); |
|---|
| 72 | |
|---|
| 73 | /** |
|---|
| 74 | * Removes a zoom change listener |
|---|
| 75 | * |
|---|
| 76 | * @param listener the listener. Ignored if null or already absent |
|---|
| 77 | */ |
|---|
| 78 | public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) { |
|---|
| 79 | zoomChangeListeners.remove(listener); |
|---|
| 80 | } |
|---|
| 81 | |
|---|
| 82 | /** |
|---|
| 83 | * Adds a zoom change listener |
|---|
| 84 | * |
|---|
| 85 | * @param listener the listener. Ignored if null or already registered. |
|---|
| 86 | */ |
|---|
| 87 | public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) { |
|---|
| 88 | if (listener != null) { |
|---|
| 89 | zoomChangeListeners.addIfAbsent(listener); |
|---|
| 90 | } |
|---|
| 91 | } |
|---|
| 92 | |
|---|
| 93 | protected static void fireZoomChanged() { |
|---|
| 94 | for (ZoomChangeListener l : zoomChangeListeners) { |
|---|
| 95 | l.zoomChanged(); |
|---|
| 96 | } |
|---|
| 97 | } |
|---|
| 98 | |
|---|
| 99 | /** |
|---|
| 100 | * The scale factor in x or y-units per pixel. This means, if scale = 10, |
|---|
| 101 | * every physical pixel on screen are 10 x or 10 y units in the |
|---|
| 102 | * northing/easting space of the projection. |
|---|
| 103 | */ |
|---|
| 104 | private double scale = Main.getProjection().getDefaultZoomInPPD(); |
|---|
| 105 | /** |
|---|
| 106 | * Center n/e coordinate of the desired screen center. |
|---|
| 107 | */ |
|---|
| 108 | protected EastNorth center = calculateDefaultCenter(); |
|---|
| 109 | |
|---|
| 110 | public NavigatableComponent() { |
|---|
| 111 | setLayout(null); |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | protected DataSet getCurrentDataSet() { |
|---|
| 115 | return Main.main.getCurrentDataSet(); |
|---|
| 116 | } |
|---|
| 117 | |
|---|
| 118 | private EastNorth calculateDefaultCenter() { |
|---|
| 119 | Bounds b = Main.getProjection().getWorldBoundsLatLon(); |
|---|
| 120 | double lat = (b.getMax().lat() + b.getMin().lat())/2; |
|---|
| 121 | double lon = (b.getMax().lon() + b.getMin().lon())/2; |
|---|
| 122 | |
|---|
| 123 | return Main.getProjection().latlon2eastNorth(new LatLon(lat, lon)); |
|---|
| 124 | } |
|---|
| 125 | |
|---|
| 126 | public static String getDistText(double dist) { |
|---|
| 127 | return getSystemOfMeasurement().getDistText(dist); |
|---|
| 128 | } |
|---|
| 129 | |
|---|
| 130 | public String getDist100PixelText() |
|---|
| 131 | { |
|---|
| 132 | return getDistText(getDist100Pixel()); |
|---|
| 133 | } |
|---|
| 134 | |
|---|
| 135 | public double getDist100Pixel() |
|---|
| 136 | { |
|---|
| 137 | int w = getWidth()/2; |
|---|
| 138 | int h = getHeight()/2; |
|---|
| 139 | LatLon ll1 = getLatLon(w-50,h); |
|---|
| 140 | LatLon ll2 = getLatLon(w+50,h); |
|---|
| 141 | return ll1.greatCircleDistance(ll2); |
|---|
| 142 | } |
|---|
| 143 | |
|---|
| 144 | /** |
|---|
| 145 | * @return Returns the center point. A copy is returned, so users cannot |
|---|
| 146 | * change the center by accessing the return value. Use zoomTo instead. |
|---|
| 147 | */ |
|---|
| 148 | public EastNorth getCenter() { |
|---|
| 149 | return center; |
|---|
| 150 | } |
|---|
| 151 | |
|---|
| 152 | /** |
|---|
| 153 | * @param x X-Pixelposition to get coordinate from |
|---|
| 154 | * @param y Y-Pixelposition to get coordinate from |
|---|
| 155 | * |
|---|
| 156 | * @return Geographic coordinates from a specific pixel coordination |
|---|
| 157 | * on the screen. |
|---|
| 158 | */ |
|---|
| 159 | public EastNorth getEastNorth(int x, int y) { |
|---|
| 160 | return new EastNorth( |
|---|
| 161 | center.east() + (x - getWidth()/2.0)*scale, |
|---|
| 162 | center.north() - (y - getHeight()/2.0)*scale); |
|---|
| 163 | } |
|---|
| 164 | |
|---|
| 165 | public ProjectionBounds getProjectionBounds() { |
|---|
| 166 | return new ProjectionBounds( |
|---|
| 167 | new EastNorth( |
|---|
| 168 | center.east() - getWidth()/2.0*scale, |
|---|
| 169 | center.north() - getHeight()/2.0*scale), |
|---|
| 170 | new EastNorth( |
|---|
| 171 | center.east() + getWidth()/2.0*scale, |
|---|
| 172 | center.north() + getHeight()/2.0*scale)); |
|---|
| 173 | } |
|---|
| 174 | |
|---|
| 175 | /* FIXME: replace with better method - used by MapSlider */ |
|---|
| 176 | public ProjectionBounds getMaxProjectionBounds() { |
|---|
| 177 | Bounds b = getProjection().getWorldBoundsLatLon(); |
|---|
| 178 | return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()), |
|---|
| 179 | getProjection().latlon2eastNorth(b.getMax())); |
|---|
| 180 | } |
|---|
| 181 | |
|---|
| 182 | /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */ |
|---|
| 183 | public Bounds getRealBounds() { |
|---|
| 184 | return new Bounds( |
|---|
| 185 | getProjection().eastNorth2latlon(new EastNorth( |
|---|
| 186 | center.east() - getWidth()/2.0*scale, |
|---|
| 187 | center.north() - getHeight()/2.0*scale)), |
|---|
| 188 | getProjection().eastNorth2latlon(new EastNorth( |
|---|
| 189 | center.east() + getWidth()/2.0*scale, |
|---|
| 190 | center.north() + getHeight()/2.0*scale))); |
|---|
| 191 | } |
|---|
| 192 | |
|---|
| 193 | /** |
|---|
| 194 | * @param x X-Pixelposition to get coordinate from |
|---|
| 195 | * @param y Y-Pixelposition to get coordinate from |
|---|
| 196 | * |
|---|
| 197 | * @return Geographic unprojected coordinates from a specific pixel coordination |
|---|
| 198 | * on the screen. |
|---|
| 199 | */ |
|---|
| 200 | public LatLon getLatLon(int x, int y) { |
|---|
| 201 | return getProjection().eastNorth2latlon(getEastNorth(x, y)); |
|---|
| 202 | } |
|---|
| 203 | |
|---|
| 204 | public LatLon getLatLon(double x, double y) { |
|---|
| 205 | return getLatLon((int)x, (int)y); |
|---|
| 206 | } |
|---|
| 207 | |
|---|
| 208 | /** |
|---|
| 209 | * @param r |
|---|
| 210 | * @return Minimum bounds that will cover rectangle |
|---|
| 211 | */ |
|---|
| 212 | public Bounds getLatLonBounds(Rectangle r) { |
|---|
| 213 | // TODO Maybe this should be (optional) method of Projection implementation |
|---|
| 214 | EastNorth p1 = getEastNorth(r.x, r.y); |
|---|
| 215 | EastNorth p2 = getEastNorth(r.x + r.width, r.y + r.height); |
|---|
| 216 | |
|---|
| 217 | Bounds result = new Bounds(Main.getProjection().eastNorth2latlon(p1)); |
|---|
| 218 | |
|---|
| 219 | double eastMin = Math.min(p1.east(), p2.east()); |
|---|
| 220 | double eastMax = Math.max(p1.east(), p2.east()); |
|---|
| 221 | double northMin = Math.min(p1.north(), p2.north()); |
|---|
| 222 | double northMax = Math.max(p1.north(), p2.north()); |
|---|
| 223 | double deltaEast = (eastMax - eastMin) / 10; |
|---|
| 224 | double deltaNorth = (northMax - northMin) / 10; |
|---|
| 225 | |
|---|
| 226 | for (int i=0; i < 10; i++) { |
|---|
| 227 | result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin))); |
|---|
| 228 | result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax))); |
|---|
| 229 | result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth))); |
|---|
| 230 | result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth))); |
|---|
| 231 | } |
|---|
| 232 | |
|---|
| 233 | return result; |
|---|
| 234 | } |
|---|
| 235 | |
|---|
| 236 | public AffineTransform getAffineTransform() { |
|---|
| 237 | return new AffineTransform( |
|---|
| 238 | 1.0/scale, 0.0, 0.0, -1.0/scale, getWidth()/2.0 - center.east()/scale, getHeight()/2.0 + center.north()/scale); |
|---|
| 239 | } |
|---|
| 240 | |
|---|
| 241 | /** |
|---|
| 242 | * Return the point on the screen where this Coordinate would be. |
|---|
| 243 | * @param p The point, where this geopoint would be drawn. |
|---|
| 244 | * @return The point on screen where "point" would be drawn, relative |
|---|
| 245 | * to the own top/left. |
|---|
| 246 | */ |
|---|
| 247 | public Point2D getPoint2D(EastNorth p) { |
|---|
| 248 | if (null == p) |
|---|
| 249 | return new Point(); |
|---|
| 250 | double x = (p.east()-center.east())/scale + getWidth()/2; |
|---|
| 251 | double y = (center.north()-p.north())/scale + getHeight()/2; |
|---|
| 252 | return new Point2D.Double(x, y); |
|---|
| 253 | } |
|---|
| 254 | |
|---|
| 255 | public Point2D getPoint2D(LatLon latlon) { |
|---|
| 256 | if (latlon == null) |
|---|
| 257 | return new Point(); |
|---|
| 258 | else if (latlon instanceof CachedLatLon) |
|---|
| 259 | return getPoint2D(((CachedLatLon)latlon).getEastNorth()); |
|---|
| 260 | else |
|---|
| 261 | return getPoint2D(getProjection().latlon2eastNorth(latlon)); |
|---|
| 262 | } |
|---|
| 263 | |
|---|
| 264 | public Point2D getPoint2D(Node n) { |
|---|
| 265 | return getPoint2D(n.getEastNorth()); |
|---|
| 266 | } |
|---|
| 267 | |
|---|
| 268 | // looses precision, may overflow (depends on p and current scale) |
|---|
| 269 | //@Deprecated |
|---|
| 270 | public Point getPoint(EastNorth p) { |
|---|
| 271 | Point2D d = getPoint2D(p); |
|---|
| 272 | return new Point((int) d.getX(), (int) d.getY()); |
|---|
| 273 | } |
|---|
| 274 | |
|---|
| 275 | // looses precision, may overflow (depends on p and current scale) |
|---|
| 276 | //@Deprecated |
|---|
| 277 | public Point getPoint(LatLon latlon) { |
|---|
| 278 | Point2D d = getPoint2D(latlon); |
|---|
| 279 | return new Point((int) d.getX(), (int) d.getY()); |
|---|
| 280 | } |
|---|
| 281 | |
|---|
| 282 | // looses precision, may overflow (depends on p and current scale) |
|---|
| 283 | //@Deprecated |
|---|
| 284 | public Point getPoint(Node n) { |
|---|
| 285 | Point2D d = getPoint2D(n); |
|---|
| 286 | return new Point((int) d.getX(), (int) d.getY()); |
|---|
| 287 | } |
|---|
| 288 | |
|---|
| 289 | /** |
|---|
| 290 | * Zoom to the given coordinate. |
|---|
| 291 | * @param newCenter The center x-value (easting) to zoom to. |
|---|
| 292 | * @param scale The scale to use. |
|---|
| 293 | */ |
|---|
| 294 | public void zoomTo(EastNorth newCenter, double newScale) { |
|---|
| 295 | Bounds b = getProjection().getWorldBoundsLatLon(); |
|---|
| 296 | LatLon cl = Projections.inverseProject(newCenter); |
|---|
| 297 | boolean changed = false; |
|---|
| 298 | double lat = cl.lat(); |
|---|
| 299 | double lon = cl.lon(); |
|---|
| 300 | if(lat < b.getMin().lat()) {changed = true; lat = b.getMin().lat(); } |
|---|
| 301 | else if(lat > b.getMax().lat()) {changed = true; lat = b.getMax().lat(); } |
|---|
| 302 | if(lon < b.getMin().lon()) {changed = true; lon = b.getMin().lon(); } |
|---|
| 303 | else if(lon > b.getMax().lon()) {changed = true; lon = b.getMax().lon(); } |
|---|
| 304 | if(changed) { |
|---|
| 305 | newCenter = Projections.project(new LatLon(lat,lon)); |
|---|
| 306 | } |
|---|
| 307 | int width = getWidth()/2; |
|---|
| 308 | int height = getHeight()/2; |
|---|
| 309 | LatLon l1 = new LatLon(b.getMin().lat(), lon); |
|---|
| 310 | LatLon l2 = new LatLon(b.getMax().lat(), lon); |
|---|
| 311 | EastNorth e1 = getProjection().latlon2eastNorth(l1); |
|---|
| 312 | EastNorth e2 = getProjection().latlon2eastNorth(l2); |
|---|
| 313 | double d = e2.north() - e1.north(); |
|---|
| 314 | if(d < height*newScale) |
|---|
| 315 | { |
|---|
| 316 | double newScaleH = d/height; |
|---|
| 317 | e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMin().lon())); |
|---|
| 318 | e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMax().lon())); |
|---|
| 319 | d = e2.east() - e1.east(); |
|---|
| 320 | if(d < width*newScale) { |
|---|
| 321 | newScale = Math.max(newScaleH, d/width); |
|---|
| 322 | } |
|---|
| 323 | } |
|---|
| 324 | else |
|---|
| 325 | { |
|---|
| 326 | d = d/(l1.greatCircleDistance(l2)*height*10); |
|---|
| 327 | if(newScale < d) { |
|---|
| 328 | newScale = d; |
|---|
| 329 | } |
|---|
| 330 | } |
|---|
| 331 | |
|---|
| 332 | if (!newCenter.equals(center) || (scale != newScale)) { |
|---|
| 333 | pushZoomUndo(center, scale); |
|---|
| 334 | zoomNoUndoTo(newCenter, newScale); |
|---|
| 335 | } |
|---|
| 336 | } |
|---|
| 337 | |
|---|
| 338 | /** |
|---|
| 339 | * Zoom to the given coordinate without adding to the zoom undo buffer. |
|---|
| 340 | * @param newCenter The center x-value (easting) to zoom to. |
|---|
| 341 | * @param scale The scale to use. |
|---|
| 342 | */ |
|---|
| 343 | private void zoomNoUndoTo(EastNorth newCenter, double newScale) { |
|---|
| 344 | if (!newCenter.equals(center)) { |
|---|
| 345 | EastNorth oldCenter = center; |
|---|
| 346 | center = newCenter; |
|---|
| 347 | firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter); |
|---|
| 348 | } |
|---|
| 349 | if (scale != newScale) { |
|---|
| 350 | double oldScale = scale; |
|---|
| 351 | scale = newScale; |
|---|
| 352 | firePropertyChange(PROPNAME_SCALE, oldScale, newScale); |
|---|
| 353 | } |
|---|
| 354 | |
|---|
| 355 | repaint(); |
|---|
| 356 | fireZoomChanged(); |
|---|
| 357 | } |
|---|
| 358 | |
|---|
| 359 | public void zoomTo(EastNorth newCenter) { |
|---|
| 360 | zoomTo(newCenter, scale); |
|---|
| 361 | } |
|---|
| 362 | |
|---|
| 363 | public void zoomTo(LatLon newCenter) { |
|---|
| 364 | zoomTo(Projections.project(newCenter)); |
|---|
| 365 | } |
|---|
| 366 | |
|---|
| 367 | public void smoothScrollTo(LatLon newCenter) { |
|---|
| 368 | smoothScrollTo(Projections.project(newCenter)); |
|---|
| 369 | } |
|---|
| 370 | |
|---|
| 371 | /** |
|---|
| 372 | * Create a thread that moves the viewport to the given center in an |
|---|
| 373 | * animated fashion. |
|---|
| 374 | */ |
|---|
| 375 | public void smoothScrollTo(EastNorth newCenter) { |
|---|
| 376 | // fixme make these configurable. |
|---|
| 377 | final int fps = 20; // animation frames per second |
|---|
| 378 | final int speed = 1500; // milliseconds for full-screen-width pan |
|---|
| 379 | if (!newCenter.equals(center)) { |
|---|
| 380 | final EastNorth oldCenter = center; |
|---|
| 381 | final double distance = newCenter.distance(oldCenter) / scale; |
|---|
| 382 | final double milliseconds = distance / getWidth() * speed; |
|---|
| 383 | final double frames = milliseconds * fps / 1000; |
|---|
| 384 | final EastNorth finalNewCenter = newCenter; |
|---|
| 385 | |
|---|
| 386 | new Thread( |
|---|
| 387 | new Runnable() { |
|---|
| 388 | public void run() { |
|---|
| 389 | for (int i=0; i<frames; i++) |
|---|
| 390 | { |
|---|
| 391 | // fixme - not use zoom history here |
|---|
| 392 | zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames)); |
|---|
| 393 | try { Thread.sleep(1000 / fps); } catch (InterruptedException ex) { }; |
|---|
| 394 | } |
|---|
| 395 | } |
|---|
| 396 | } |
|---|
| 397 | ).start(); |
|---|
| 398 | } |
|---|
| 399 | } |
|---|
| 400 | |
|---|
| 401 | public void zoomToFactor(double x, double y, double factor) { |
|---|
| 402 | double newScale = scale*factor; |
|---|
| 403 | // New center position so that point under the mouse pointer stays the same place as it was before zooming |
|---|
| 404 | // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom |
|---|
| 405 | zoomTo(new EastNorth( |
|---|
| 406 | center.east() - (x - getWidth()/2.0) * (newScale - scale), |
|---|
| 407 | center.north() + (y - getHeight()/2.0) * (newScale - scale)), |
|---|
| 408 | newScale); |
|---|
| 409 | } |
|---|
| 410 | |
|---|
| 411 | public void zoomToFactor(EastNorth newCenter, double factor) { |
|---|
| 412 | zoomTo(newCenter, scale*factor); |
|---|
| 413 | } |
|---|
| 414 | |
|---|
| 415 | public void zoomToFactor(double factor) { |
|---|
| 416 | zoomTo(center, scale*factor); |
|---|
| 417 | } |
|---|
| 418 | |
|---|
| 419 | public void zoomTo(ProjectionBounds box) { |
|---|
| 420 | // -20 to leave some border |
|---|
| 421 | int w = getWidth()-20; |
|---|
| 422 | if (w < 20) { |
|---|
| 423 | w = 20; |
|---|
| 424 | } |
|---|
| 425 | int h = getHeight()-20; |
|---|
| 426 | if (h < 20) { |
|---|
| 427 | h = 20; |
|---|
| 428 | } |
|---|
| 429 | |
|---|
| 430 | double scaleX = (box.maxEast-box.minEast)/w; |
|---|
| 431 | double scaleY = (box.maxNorth-box.minNorth)/h; |
|---|
| 432 | double newScale = Math.max(scaleX, scaleY); |
|---|
| 433 | |
|---|
| 434 | zoomTo(box.getCenter(), newScale); |
|---|
| 435 | } |
|---|
| 436 | |
|---|
| 437 | public void zoomTo(Bounds box) { |
|---|
| 438 | zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()), |
|---|
| 439 | getProjection().latlon2eastNorth(box.getMax()))); |
|---|
| 440 | } |
|---|
| 441 | |
|---|
| 442 | private class ZoomData { |
|---|
| 443 | LatLon center; |
|---|
| 444 | double scale; |
|---|
| 445 | |
|---|
| 446 | public ZoomData(EastNorth center, double scale) { |
|---|
| 447 | this.center = Projections.inverseProject(center); |
|---|
| 448 | this.scale = scale; |
|---|
| 449 | } |
|---|
| 450 | |
|---|
| 451 | public EastNorth getCenterEastNorth() { |
|---|
| 452 | return getProjection().latlon2eastNorth(center); |
|---|
| 453 | } |
|---|
| 454 | |
|---|
| 455 | public double getScale() { |
|---|
| 456 | return scale; |
|---|
| 457 | } |
|---|
| 458 | } |
|---|
| 459 | |
|---|
| 460 | private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>(); |
|---|
| 461 | private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>(); |
|---|
| 462 | private Date zoomTimestamp = new Date(); |
|---|
| 463 | |
|---|
| 464 | private void pushZoomUndo(EastNorth center, double scale) { |
|---|
| 465 | Date now = new Date(); |
|---|
| 466 | if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) { |
|---|
| 467 | zoomUndoBuffer.push(new ZoomData(center, scale)); |
|---|
| 468 | if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) { |
|---|
| 469 | zoomUndoBuffer.remove(0); |
|---|
| 470 | } |
|---|
| 471 | zoomRedoBuffer.clear(); |
|---|
| 472 | } |
|---|
| 473 | zoomTimestamp = now; |
|---|
| 474 | } |
|---|
| 475 | |
|---|
| 476 | public void zoomPrevious() { |
|---|
| 477 | if (!zoomUndoBuffer.isEmpty()) { |
|---|
| 478 | ZoomData zoom = zoomUndoBuffer.pop(); |
|---|
| 479 | zoomRedoBuffer.push(new ZoomData(center, scale)); |
|---|
| 480 | zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale()); |
|---|
| 481 | } |
|---|
| 482 | } |
|---|
| 483 | |
|---|
| 484 | public void zoomNext() { |
|---|
| 485 | if (!zoomRedoBuffer.isEmpty()) { |
|---|
| 486 | ZoomData zoom = zoomRedoBuffer.pop(); |
|---|
| 487 | zoomUndoBuffer.push(new ZoomData(center, scale)); |
|---|
| 488 | zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale()); |
|---|
| 489 | } |
|---|
| 490 | } |
|---|
| 491 | |
|---|
| 492 | public boolean hasZoomUndoEntries() { |
|---|
| 493 | return !zoomUndoBuffer.isEmpty(); |
|---|
| 494 | } |
|---|
| 495 | |
|---|
| 496 | public boolean hasZoomRedoEntries() { |
|---|
| 497 | return !zoomRedoBuffer.isEmpty(); |
|---|
| 498 | } |
|---|
| 499 | |
|---|
| 500 | private BBox getBBox(Point p, int snapDistance) { |
|---|
| 501 | return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance), |
|---|
| 502 | getLatLon(p.x + snapDistance, p.y + snapDistance)); |
|---|
| 503 | } |
|---|
| 504 | |
|---|
| 505 | /** |
|---|
| 506 | * The *result* does not depend on the current map selection state, |
|---|
| 507 | * neither does the result *order*. |
|---|
| 508 | * It solely depends on the distance to point p. |
|---|
| 509 | * |
|---|
| 510 | * @return a sorted map with the keys representing the distance of |
|---|
| 511 | * their associated nodes to point p. |
|---|
| 512 | */ |
|---|
| 513 | private Map<Double, List<Node>> getNearestNodesImpl(Point p, |
|---|
| 514 | Predicate<OsmPrimitive> predicate) { |
|---|
| 515 | TreeMap<Double, List<Node>> nearestMap = new TreeMap<Double, List<Node>>(); |
|---|
| 516 | DataSet ds = getCurrentDataSet(); |
|---|
| 517 | |
|---|
| 518 | if (ds != null) { |
|---|
| 519 | double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get(); |
|---|
| 520 | snapDistanceSq *= snapDistanceSq; |
|---|
| 521 | |
|---|
| 522 | for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) { |
|---|
| 523 | if (predicate.evaluate(n) |
|---|
| 524 | && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) |
|---|
| 525 | { |
|---|
| 526 | List<Node> nlist; |
|---|
| 527 | if (nearestMap.containsKey(dist)) { |
|---|
| 528 | nlist = nearestMap.get(dist); |
|---|
| 529 | } else { |
|---|
| 530 | nlist = new LinkedList<Node>(); |
|---|
| 531 | nearestMap.put(dist, nlist); |
|---|
| 532 | } |
|---|
| 533 | nlist.add(n); |
|---|
| 534 | } |
|---|
| 535 | } |
|---|
| 536 | } |
|---|
| 537 | |
|---|
| 538 | return nearestMap; |
|---|
| 539 | } |
|---|
| 540 | |
|---|
| 541 | /** |
|---|
| 542 | * The *result* does not depend on the current map selection state, |
|---|
| 543 | * neither does the result *order*. |
|---|
| 544 | * It solely depends on the distance to point p. |
|---|
| 545 | * |
|---|
| 546 | * @return All nodes nearest to point p that are in a belt from |
|---|
| 547 | * dist(nearest) to dist(nearest)+4px around p and |
|---|
| 548 | * that are not in ignore. |
|---|
| 549 | * |
|---|
| 550 | * @param p the point for which to search the nearest segment. |
|---|
| 551 | * @param ignore a collection of nodes which are not to be returned. |
|---|
| 552 | * @param predicate the returned objects have to fulfill certain properties. |
|---|
| 553 | */ |
|---|
| 554 | public final List<Node> getNearestNodes(Point p, |
|---|
| 555 | Collection<Node> ignore, Predicate<OsmPrimitive> predicate) { |
|---|
| 556 | List<Node> nearestList = Collections.emptyList(); |
|---|
| 557 | |
|---|
| 558 | if (ignore == null) { |
|---|
| 559 | ignore = Collections.emptySet(); |
|---|
| 560 | } |
|---|
| 561 | |
|---|
| 562 | Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate); |
|---|
| 563 | if (!nlists.isEmpty()) { |
|---|
| 564 | Double minDistSq = null; |
|---|
| 565 | List<Node> nlist; |
|---|
| 566 | for (Double distSq : nlists.keySet()) { |
|---|
| 567 | nlist = nlists.get(distSq); |
|---|
| 568 | |
|---|
| 569 | // filter nodes to be ignored before determining minDistSq.. |
|---|
| 570 | nlist.removeAll(ignore); |
|---|
| 571 | if (minDistSq == null) { |
|---|
| 572 | if (!nlist.isEmpty()) { |
|---|
| 573 | minDistSq = distSq; |
|---|
| 574 | nearestList = new ArrayList<Node>(); |
|---|
| 575 | nearestList.addAll(nlist); |
|---|
| 576 | } |
|---|
| 577 | } else { |
|---|
| 578 | if (distSq-minDistSq < (4)*(4)) { |
|---|
| 579 | nearestList.addAll(nlist); |
|---|
| 580 | } |
|---|
| 581 | } |
|---|
| 582 | } |
|---|
| 583 | } |
|---|
| 584 | |
|---|
| 585 | return nearestList; |
|---|
| 586 | } |
|---|
| 587 | |
|---|
| 588 | /** |
|---|
| 589 | * The *result* does not depend on the current map selection state, |
|---|
| 590 | * neither does the result *order*. |
|---|
| 591 | * It solely depends on the distance to point p. |
|---|
| 592 | * |
|---|
| 593 | * @return All nodes nearest to point p that are in a belt from |
|---|
| 594 | * dist(nearest) to dist(nearest)+4px around p. |
|---|
| 595 | * @see #getNearestNodes(Point, Collection, Predicate) |
|---|
| 596 | * |
|---|
| 597 | * @param p the point for which to search the nearest segment. |
|---|
| 598 | * @param predicate the returned objects have to fulfill certain properties. |
|---|
| 599 | */ |
|---|
| 600 | public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 601 | return getNearestNodes(p, null, predicate); |
|---|
| 602 | } |
|---|
| 603 | |
|---|
| 604 | /** |
|---|
| 605 | * The *result* depends on the current map selection state IF use_selected is true. |
|---|
| 606 | * |
|---|
| 607 | * If more than one node within node.snap-distance pixels is found, |
|---|
| 608 | * the nearest node selected is returned IF use_selected is true. |
|---|
| 609 | * |
|---|
| 610 | * Else the nearest new/id=0 node within about the same distance |
|---|
| 611 | * as the true nearest node is returned. |
|---|
| 612 | * |
|---|
| 613 | * If no such node is found either, the true nearest |
|---|
| 614 | * node to p is returned. |
|---|
| 615 | * |
|---|
| 616 | * Finally, if a node is not found at all, null is returned. |
|---|
| 617 | * |
|---|
| 618 | * @return A node within snap-distance to point p, |
|---|
| 619 | * that is chosen by the algorithm described. |
|---|
| 620 | * |
|---|
| 621 | * @param p the screen point |
|---|
| 622 | * @param predicate this parameter imposes a condition on the returned object, e.g. |
|---|
| 623 | * give the nearest node that is tagged. |
|---|
| 624 | */ |
|---|
| 625 | public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) { |
|---|
| 626 | Node n = null; |
|---|
| 627 | |
|---|
| 628 | Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate); |
|---|
| 629 | if (!nlists.isEmpty()) { |
|---|
| 630 | Node ntsel = null, ntnew = null; |
|---|
| 631 | double minDistSq = nlists.keySet().iterator().next(); |
|---|
| 632 | |
|---|
| 633 | for (Double distSq : nlists.keySet()) { |
|---|
| 634 | for (Node nd : nlists.get(distSq)) { |
|---|
| 635 | // find the nearest selected node |
|---|
| 636 | if (ntsel == null && nd.isSelected()) { |
|---|
| 637 | ntsel = nd; |
|---|
| 638 | // if there are multiple nearest nodes, prefer the one |
|---|
| 639 | // that is selected. This is required in order to drag |
|---|
| 640 | // the selected node if multiple nodes have the same |
|---|
| 641 | // coordinates (e.g. after unglue) |
|---|
| 642 | use_selected |= (distSq == minDistSq); |
|---|
| 643 | } |
|---|
| 644 | // find the nearest newest node that is within about the same |
|---|
| 645 | // distance as the true nearest node |
|---|
| 646 | if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) { |
|---|
| 647 | ntnew = nd; |
|---|
| 648 | } |
|---|
| 649 | } |
|---|
| 650 | } |
|---|
| 651 | |
|---|
| 652 | // take nearest selected, nearest new or true nearest node to p, in that order |
|---|
| 653 | n = (ntsel != null && use_selected) ? ntsel |
|---|
| 654 | : (ntnew != null) ? ntnew |
|---|
| 655 | : nlists.values().iterator().next().get(0); |
|---|
| 656 | } |
|---|
| 657 | return n; |
|---|
| 658 | } |
|---|
| 659 | |
|---|
| 660 | /** |
|---|
| 661 | * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}. |
|---|
| 662 | * |
|---|
| 663 | * @return The nearest node to point p. |
|---|
| 664 | */ |
|---|
| 665 | public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 666 | return getNearestNode(p, predicate, true); |
|---|
| 667 | } |
|---|
| 668 | |
|---|
| 669 | /** |
|---|
| 670 | * The *result* does not depend on the current map selection state, |
|---|
| 671 | * neither does the result *order*. |
|---|
| 672 | * It solely depends on the distance to point p. |
|---|
| 673 | * |
|---|
| 674 | * @return a sorted map with the keys representing the perpendicular |
|---|
| 675 | * distance of their associated way segments to point p. |
|---|
| 676 | */ |
|---|
| 677 | private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, |
|---|
| 678 | Predicate<OsmPrimitive> predicate) { |
|---|
| 679 | Map<Double, List<WaySegment>> nearestMap = new TreeMap<Double, List<WaySegment>>(); |
|---|
| 680 | DataSet ds = getCurrentDataSet(); |
|---|
| 681 | |
|---|
| 682 | if (ds != null) { |
|---|
| 683 | double snapDistanceSq = Main.pref.getInteger("mappaint.segment.snap-distance", 10); |
|---|
| 684 | snapDistanceSq *= snapDistanceSq; |
|---|
| 685 | |
|---|
| 686 | for (Way w : ds.searchWays(getBBox(p, Main.pref.getInteger("mappaint.segment.snap-distance", 10)))) { |
|---|
| 687 | if (!predicate.evaluate(w)) { |
|---|
| 688 | continue; |
|---|
| 689 | } |
|---|
| 690 | Node lastN = null; |
|---|
| 691 | int i = -2; |
|---|
| 692 | for (Node n : w.getNodes()) { |
|---|
| 693 | i++; |
|---|
| 694 | if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception? |
|---|
| 695 | continue; |
|---|
| 696 | } |
|---|
| 697 | if (lastN == null) { |
|---|
| 698 | lastN = n; |
|---|
| 699 | continue; |
|---|
| 700 | } |
|---|
| 701 | |
|---|
| 702 | Point2D A = getPoint2D(lastN); |
|---|
| 703 | Point2D B = getPoint2D(n); |
|---|
| 704 | double c = A.distanceSq(B); |
|---|
| 705 | double a = p.distanceSq(B); |
|---|
| 706 | double b = p.distanceSq(A); |
|---|
| 707 | |
|---|
| 708 | /* perpendicular distance squared |
|---|
| 709 | * loose some precision to account for possible deviations in the calculation above |
|---|
| 710 | * e.g. if identical (A and B) come about reversed in another way, values may differ |
|---|
| 711 | * -- zero out least significant 32 dual digits of mantissa.. |
|---|
| 712 | */ |
|---|
| 713 | double perDistSq = Double.longBitsToDouble( |
|---|
| 714 | Double.doubleToLongBits( a - (a - b + c) * (a - b + c) / 4 / c ) |
|---|
| 715 | >> 32 << 32); // resolution in numbers with large exponent not needed here.. |
|---|
| 716 | |
|---|
| 717 | if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) { |
|---|
| 718 | //System.err.println(Double.toHexString(perDistSq)); |
|---|
| 719 | |
|---|
| 720 | List<WaySegment> wslist; |
|---|
| 721 | if (nearestMap.containsKey(perDistSq)) { |
|---|
| 722 | wslist = nearestMap.get(perDistSq); |
|---|
| 723 | } else { |
|---|
| 724 | wslist = new LinkedList<WaySegment>(); |
|---|
| 725 | nearestMap.put(perDistSq, wslist); |
|---|
| 726 | } |
|---|
| 727 | wslist.add(new WaySegment(w, i)); |
|---|
| 728 | } |
|---|
| 729 | |
|---|
| 730 | lastN = n; |
|---|
| 731 | } |
|---|
| 732 | } |
|---|
| 733 | } |
|---|
| 734 | |
|---|
| 735 | return nearestMap; |
|---|
| 736 | } |
|---|
| 737 | |
|---|
| 738 | /** |
|---|
| 739 | * The result *order* depends on the current map selection state. |
|---|
| 740 | * Segments within 10px of p are searched and sorted by their distance to @param p, |
|---|
| 741 | * then, within groups of equally distant segments, prefer those that are selected. |
|---|
| 742 | * |
|---|
| 743 | * @return all segments within 10px of p that are not in ignore, |
|---|
| 744 | * sorted by their perpendicular distance. |
|---|
| 745 | * |
|---|
| 746 | * @param p the point for which to search the nearest segments. |
|---|
| 747 | * @param ignore a collection of segments which are not to be returned. |
|---|
| 748 | * @param predicate the returned objects have to fulfill certain properties. |
|---|
| 749 | */ |
|---|
| 750 | public final List<WaySegment> getNearestWaySegments(Point p, |
|---|
| 751 | Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) { |
|---|
| 752 | List<WaySegment> nearestList = new ArrayList<WaySegment>(); |
|---|
| 753 | List<WaySegment> unselected = new LinkedList<WaySegment>(); |
|---|
| 754 | |
|---|
| 755 | for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { |
|---|
| 756 | // put selected waysegs within each distance group first |
|---|
| 757 | // makes the order of nearestList dependent on current selection state |
|---|
| 758 | for (WaySegment ws : wss) { |
|---|
| 759 | (ws.way.isSelected() ? nearestList : unselected).add(ws); |
|---|
| 760 | } |
|---|
| 761 | nearestList.addAll(unselected); |
|---|
| 762 | unselected.clear(); |
|---|
| 763 | } |
|---|
| 764 | if (ignore != null) { |
|---|
| 765 | nearestList.removeAll(ignore); |
|---|
| 766 | } |
|---|
| 767 | |
|---|
| 768 | return nearestList; |
|---|
| 769 | } |
|---|
| 770 | |
|---|
| 771 | /** |
|---|
| 772 | * The result *order* depends on the current map selection state. |
|---|
| 773 | * |
|---|
| 774 | * @return all segments within 10px of p, sorted by their perpendicular distance. |
|---|
| 775 | * @see #getNearestWaySegments(Point, Collection, Predicate) |
|---|
| 776 | * |
|---|
| 777 | * @param p the point for which to search the nearest segments. |
|---|
| 778 | * @param predicate the returned objects have to fulfill certain properties. |
|---|
| 779 | */ |
|---|
| 780 | public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 781 | return getNearestWaySegments(p, null, predicate); |
|---|
| 782 | } |
|---|
| 783 | |
|---|
| 784 | /** |
|---|
| 785 | * The *result* depends on the current map selection state IF use_selected is true. |
|---|
| 786 | * |
|---|
| 787 | * @return The nearest way segment to point p, |
|---|
| 788 | * and, depending on use_selected, prefers a selected way segment, if found. |
|---|
| 789 | * @see #getNearestWaySegments(Point, Collection, Predicate) |
|---|
| 790 | * |
|---|
| 791 | * @param p the point for which to search the nearest segment. |
|---|
| 792 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 793 | * @param use_selected whether selected way segments should be preferred. |
|---|
| 794 | */ |
|---|
| 795 | public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) { |
|---|
| 796 | WaySegment wayseg = null, ntsel = null; |
|---|
| 797 | |
|---|
| 798 | for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) { |
|---|
| 799 | if (wayseg != null && ntsel != null) { |
|---|
| 800 | break; |
|---|
| 801 | } |
|---|
| 802 | for (WaySegment ws : wslist) { |
|---|
| 803 | if (wayseg == null) { |
|---|
| 804 | wayseg = ws; |
|---|
| 805 | } |
|---|
| 806 | if (ntsel == null && ws.way.isSelected()) { |
|---|
| 807 | ntsel = ws; |
|---|
| 808 | } |
|---|
| 809 | } |
|---|
| 810 | } |
|---|
| 811 | |
|---|
| 812 | return (ntsel != null && use_selected) ? ntsel : wayseg; |
|---|
| 813 | } |
|---|
| 814 | |
|---|
| 815 | /** |
|---|
| 816 | * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}. |
|---|
| 817 | * |
|---|
| 818 | * @return The nearest way segment to point p. |
|---|
| 819 | */ |
|---|
| 820 | public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 821 | return getNearestWaySegment(p, predicate, true); |
|---|
| 822 | } |
|---|
| 823 | |
|---|
| 824 | /** |
|---|
| 825 | * The *result* does not depend on the current map selection state, |
|---|
| 826 | * neither does the result *order*. |
|---|
| 827 | * It solely depends on the perpendicular distance to point p. |
|---|
| 828 | * |
|---|
| 829 | * @return all nearest ways to the screen point given that are not in ignore. |
|---|
| 830 | * @see #getNearestWaySegments(Point, Collection, Predicate) |
|---|
| 831 | * |
|---|
| 832 | * @param p the point for which to search the nearest ways. |
|---|
| 833 | * @param ignore a collection of ways which are not to be returned. |
|---|
| 834 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 835 | */ |
|---|
| 836 | public final List<Way> getNearestWays(Point p, |
|---|
| 837 | Collection<Way> ignore, Predicate<OsmPrimitive> predicate) { |
|---|
| 838 | List<Way> nearestList = new ArrayList<Way>(); |
|---|
| 839 | Set<Way> wset = new HashSet<Way>(); |
|---|
| 840 | |
|---|
| 841 | for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { |
|---|
| 842 | for (WaySegment ws : wss) { |
|---|
| 843 | if (wset.add(ws.way)) { |
|---|
| 844 | nearestList.add(ws.way); |
|---|
| 845 | } |
|---|
| 846 | } |
|---|
| 847 | } |
|---|
| 848 | if (ignore != null) { |
|---|
| 849 | nearestList.removeAll(ignore); |
|---|
| 850 | } |
|---|
| 851 | |
|---|
| 852 | return nearestList; |
|---|
| 853 | } |
|---|
| 854 | |
|---|
| 855 | /** |
|---|
| 856 | * The *result* does not depend on the current map selection state, |
|---|
| 857 | * neither does the result *order*. |
|---|
| 858 | * It solely depends on the perpendicular distance to point p. |
|---|
| 859 | * |
|---|
| 860 | * @return all nearest ways to the screen point given. |
|---|
| 861 | * @see #getNearestWays(Point, Collection, Predicate) |
|---|
| 862 | * |
|---|
| 863 | * @param p the point for which to search the nearest ways. |
|---|
| 864 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 865 | */ |
|---|
| 866 | public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 867 | return getNearestWays(p, null, predicate); |
|---|
| 868 | } |
|---|
| 869 | |
|---|
| 870 | /** |
|---|
| 871 | * The *result* depends on the current map selection state. |
|---|
| 872 | * |
|---|
| 873 | * @return The nearest way to point p, |
|---|
| 874 | * prefer a selected way if there are multiple nearest. |
|---|
| 875 | * @see #getNearestWaySegment(Point, Collection, Predicate) |
|---|
| 876 | * |
|---|
| 877 | * @param p the point for which to search the nearest segment. |
|---|
| 878 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 879 | */ |
|---|
| 880 | public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 881 | WaySegment nearestWaySeg = getNearestWaySegment(p, predicate); |
|---|
| 882 | return (nearestWaySeg == null) ? null : nearestWaySeg.way; |
|---|
| 883 | } |
|---|
| 884 | |
|---|
| 885 | /** |
|---|
| 886 | * The *result* does not depend on the current map selection state, |
|---|
| 887 | * neither does the result *order*. |
|---|
| 888 | * It solely depends on the distance to point p. |
|---|
| 889 | * |
|---|
| 890 | * First, nodes will be searched. If there are nodes within BBox found, |
|---|
| 891 | * return a collection of those nodes only. |
|---|
| 892 | * |
|---|
| 893 | * If no nodes are found, search for nearest ways. If there are ways |
|---|
| 894 | * within BBox found, return a collection of those ways only. |
|---|
| 895 | * |
|---|
| 896 | * If nothing is found, return an empty collection. |
|---|
| 897 | * |
|---|
| 898 | * @return Primitives nearest to the given screen point that are not in ignore. |
|---|
| 899 | * @see #getNearestNodes(Point, Collection, Predicate) |
|---|
| 900 | * @see #getNearestWays(Point, Collection, Predicate) |
|---|
| 901 | * |
|---|
| 902 | * @param p The point on screen. |
|---|
| 903 | * @param ignore a collection of ways which are not to be returned. |
|---|
| 904 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 905 | */ |
|---|
| 906 | public final List<OsmPrimitive> getNearestNodesOrWays(Point p, |
|---|
| 907 | Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) { |
|---|
| 908 | List<OsmPrimitive> nearestList = Collections.emptyList(); |
|---|
| 909 | OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false); |
|---|
| 910 | |
|---|
| 911 | if (osm != null) { |
|---|
| 912 | if (osm instanceof Node) { |
|---|
| 913 | nearestList = new ArrayList<OsmPrimitive>(getNearestNodes(p, predicate)); |
|---|
| 914 | } else if (osm instanceof Way) { |
|---|
| 915 | nearestList = new ArrayList<OsmPrimitive>(getNearestWays(p, predicate)); |
|---|
| 916 | } |
|---|
| 917 | if (ignore != null) { |
|---|
| 918 | nearestList.removeAll(ignore); |
|---|
| 919 | } |
|---|
| 920 | } |
|---|
| 921 | |
|---|
| 922 | return nearestList; |
|---|
| 923 | } |
|---|
| 924 | |
|---|
| 925 | /** |
|---|
| 926 | * The *result* does not depend on the current map selection state, |
|---|
| 927 | * neither does the result *order*. |
|---|
| 928 | * It solely depends on the distance to point p. |
|---|
| 929 | * |
|---|
| 930 | * @return Primitives nearest to the given screen point. |
|---|
| 931 | * @see #getNearests(Point, Collection, Predicate) |
|---|
| 932 | * |
|---|
| 933 | * @param p The point on screen. |
|---|
| 934 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 935 | */ |
|---|
| 936 | public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 937 | return getNearestNodesOrWays(p, null, predicate); |
|---|
| 938 | } |
|---|
| 939 | |
|---|
| 940 | /** |
|---|
| 941 | * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)} |
|---|
| 942 | * It decides, whether to yield the node to be tested or look for further (way) candidates. |
|---|
| 943 | * |
|---|
| 944 | * @return true, if the node fulfills the properties of the function body |
|---|
| 945 | * |
|---|
| 946 | * @param osm node to check |
|---|
| 947 | * @param p point clicked |
|---|
| 948 | * @param use_selected whether to prefer selected nodes |
|---|
| 949 | */ |
|---|
| 950 | private boolean isPrecedenceNode(Node osm, Point p, boolean use_selected) { |
|---|
| 951 | boolean ret = false; |
|---|
| 952 | |
|---|
| 953 | if (osm != null) { |
|---|
| 954 | ret |= !(p.distanceSq(getPoint2D(osm)) > (4)*(4)); |
|---|
| 955 | ret |= osm.isTagged(); |
|---|
| 956 | if (use_selected) { |
|---|
| 957 | ret |= osm.isSelected(); |
|---|
| 958 | } |
|---|
| 959 | } |
|---|
| 960 | |
|---|
| 961 | return ret; |
|---|
| 962 | } |
|---|
| 963 | |
|---|
| 964 | /** |
|---|
| 965 | * The *result* depends on the current map selection state IF use_selected is true. |
|---|
| 966 | * |
|---|
| 967 | * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find |
|---|
| 968 | * the nearest, selected node. If not found, try {@link #getNearestWaySegment(Point, Predicate)} |
|---|
| 969 | * to find the nearest selected way. |
|---|
| 970 | * |
|---|
| 971 | * IF use_selected is false, or if no selected primitive was found, do the following. |
|---|
| 972 | * |
|---|
| 973 | * If the nearest node found is within 4px of p, simply take it. |
|---|
| 974 | * Else, find the nearest way segment. Then, if p is closer to its |
|---|
| 975 | * middle than to the node, take the way segment, else take the node. |
|---|
| 976 | * |
|---|
| 977 | * Finally, if no nearest primitive is found at all, return null. |
|---|
| 978 | * |
|---|
| 979 | * @return A primitive within snap-distance to point p, |
|---|
| 980 | * that is chosen by the algorithm described. |
|---|
| 981 | * @see getNearestNode(Point, Predicate) |
|---|
| 982 | * @see getNearestNodesImpl(Point, Predicate) |
|---|
| 983 | * @see getNearestWay(Point, Predicate) |
|---|
| 984 | * |
|---|
| 985 | * @param p The point on screen. |
|---|
| 986 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 987 | * @param use_selected whether to prefer primitives that are currently selected. |
|---|
| 988 | */ |
|---|
| 989 | public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) { |
|---|
| 990 | OsmPrimitive osm = getNearestNode(p, predicate, use_selected); |
|---|
| 991 | WaySegment ws = null; |
|---|
| 992 | |
|---|
| 993 | if (!isPrecedenceNode((Node)osm, p, use_selected)) { |
|---|
| 994 | ws = getNearestWaySegment(p, predicate, use_selected); |
|---|
| 995 | |
|---|
| 996 | if (ws != null) { |
|---|
| 997 | if ((ws.way.isSelected() && use_selected) || osm == null) { |
|---|
| 998 | // either (no _selected_ nearest node found, if desired) or no nearest node was found |
|---|
| 999 | osm = ws.way; |
|---|
| 1000 | } else { |
|---|
| 1001 | int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get(); |
|---|
| 1002 | maxWaySegLenSq *= maxWaySegLenSq; |
|---|
| 1003 | |
|---|
| 1004 | Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex)); |
|---|
| 1005 | Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1)); |
|---|
| 1006 | |
|---|
| 1007 | // is wayseg shorter than maxWaySegLenSq and |
|---|
| 1008 | // is p closer to the middle of wayseg than to the nearest node? |
|---|
| 1009 | if (wp1.distanceSq(wp2) < maxWaySegLenSq && |
|---|
| 1010 | p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node)osm))) { |
|---|
| 1011 | osm = ws.way; |
|---|
| 1012 | } |
|---|
| 1013 | } |
|---|
| 1014 | } |
|---|
| 1015 | } |
|---|
| 1016 | |
|---|
| 1017 | return osm; |
|---|
| 1018 | } |
|---|
| 1019 | |
|---|
| 1020 | /** |
|---|
| 1021 | * @return o as collection of o's type. |
|---|
| 1022 | */ |
|---|
| 1023 | public static <T> Collection<T> asColl(T o) { |
|---|
| 1024 | if (o == null) |
|---|
| 1025 | return Collections.emptySet(); |
|---|
| 1026 | return Collections.singleton(o); |
|---|
| 1027 | } |
|---|
| 1028 | |
|---|
| 1029 | public static double perDist(Point2D pt, Point2D a, Point2D b) { |
|---|
| 1030 | if (pt != null && a != null && b != null) { |
|---|
| 1031 | double pd = ( |
|---|
| 1032 | (a.getX()-pt.getX())*(b.getX()-a.getX()) - |
|---|
| 1033 | (a.getY()-pt.getY())*(b.getY()-a.getY()) ); |
|---|
| 1034 | return Math.abs(pd) / a.distance(b); |
|---|
| 1035 | } |
|---|
| 1036 | return 0d; |
|---|
| 1037 | } |
|---|
| 1038 | |
|---|
| 1039 | /** |
|---|
| 1040 | * |
|---|
| 1041 | * @param pt point to project onto (ab) |
|---|
| 1042 | * @param a root of vector |
|---|
| 1043 | * @param b vector |
|---|
| 1044 | * @return point of intersection of line given by (ab) |
|---|
| 1045 | * with its orthogonal line running through pt |
|---|
| 1046 | */ |
|---|
| 1047 | public static Point2D project(Point2D pt, Point2D a, Point2D b) { |
|---|
| 1048 | if (pt != null && a != null && b != null) { |
|---|
| 1049 | double r = (( |
|---|
| 1050 | (pt.getX()-a.getX())*(b.getX()-a.getX()) + |
|---|
| 1051 | (pt.getY()-a.getY())*(b.getY()-a.getY()) ) |
|---|
| 1052 | / a.distanceSq(b)); |
|---|
| 1053 | return project(r, a, b); |
|---|
| 1054 | } |
|---|
| 1055 | return null; |
|---|
| 1056 | } |
|---|
| 1057 | |
|---|
| 1058 | /** |
|---|
| 1059 | * if r = 0 returns a, if r=1 returns b, |
|---|
| 1060 | * if r = 0.5 returns center between a and b, etc.. |
|---|
| 1061 | * |
|---|
| 1062 | * @param r scale value |
|---|
| 1063 | * @param a root of vector |
|---|
| 1064 | * @param b vector |
|---|
| 1065 | * @return new point at a + r*(ab) |
|---|
| 1066 | */ |
|---|
| 1067 | public static Point2D project(double r, Point2D a, Point2D b) { |
|---|
| 1068 | Point2D ret = null; |
|---|
| 1069 | |
|---|
| 1070 | if (a != null && b != null) { |
|---|
| 1071 | ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()), |
|---|
| 1072 | a.getY() + r*(b.getY()-a.getY())); |
|---|
| 1073 | } |
|---|
| 1074 | return ret; |
|---|
| 1075 | } |
|---|
| 1076 | |
|---|
| 1077 | /** |
|---|
| 1078 | * The *result* does not depend on the current map selection state, |
|---|
| 1079 | * neither does the result *order*. |
|---|
| 1080 | * It solely depends on the distance to point p. |
|---|
| 1081 | * |
|---|
| 1082 | * @return a list of all objects that are nearest to point p and |
|---|
| 1083 | * not in ignore or an empty list if nothing was found. |
|---|
| 1084 | * |
|---|
| 1085 | * @param p The point on screen. |
|---|
| 1086 | * @param ignore a collection of ways which are not to be returned. |
|---|
| 1087 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 1088 | */ |
|---|
| 1089 | public final List<OsmPrimitive> getAllNearest(Point p, |
|---|
| 1090 | Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) { |
|---|
| 1091 | List<OsmPrimitive> nearestList = new ArrayList<OsmPrimitive>(); |
|---|
| 1092 | Set<Way> wset = new HashSet<Way>(); |
|---|
| 1093 | |
|---|
| 1094 | // add nearby ways |
|---|
| 1095 | for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) { |
|---|
| 1096 | for (WaySegment ws : wss) { |
|---|
| 1097 | if (wset.add(ws.way)) { |
|---|
| 1098 | nearestList.add(ws.way); |
|---|
| 1099 | } |
|---|
| 1100 | } |
|---|
| 1101 | } |
|---|
| 1102 | |
|---|
| 1103 | // add nearby nodes |
|---|
| 1104 | for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) { |
|---|
| 1105 | nearestList.addAll(nlist); |
|---|
| 1106 | } |
|---|
| 1107 | |
|---|
| 1108 | // add parent relations of nearby nodes and ways |
|---|
| 1109 | Set<OsmPrimitive> parentRelations = new HashSet<OsmPrimitive>(); |
|---|
| 1110 | for (OsmPrimitive o : nearestList) { |
|---|
| 1111 | for (OsmPrimitive r : o.getReferrers()) { |
|---|
| 1112 | if (r instanceof Relation && predicate.evaluate(r)) { |
|---|
| 1113 | parentRelations.add(r); |
|---|
| 1114 | } |
|---|
| 1115 | } |
|---|
| 1116 | } |
|---|
| 1117 | nearestList.addAll(parentRelations); |
|---|
| 1118 | |
|---|
| 1119 | if (ignore != null) { |
|---|
| 1120 | nearestList.removeAll(ignore); |
|---|
| 1121 | } |
|---|
| 1122 | |
|---|
| 1123 | return nearestList; |
|---|
| 1124 | } |
|---|
| 1125 | |
|---|
| 1126 | /** |
|---|
| 1127 | * The *result* does not depend on the current map selection state, |
|---|
| 1128 | * neither does the result *order*. |
|---|
| 1129 | * It solely depends on the distance to point p. |
|---|
| 1130 | * |
|---|
| 1131 | * @return a list of all objects that are nearest to point p |
|---|
| 1132 | * or an empty list if nothing was found. |
|---|
| 1133 | * @see #getAllNearest(Point, Collection, Predicate) |
|---|
| 1134 | * |
|---|
| 1135 | * @param p The point on screen. |
|---|
| 1136 | * @param predicate the returned object has to fulfill certain properties. |
|---|
| 1137 | */ |
|---|
| 1138 | public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) { |
|---|
| 1139 | return getAllNearest(p, null, predicate); |
|---|
| 1140 | } |
|---|
| 1141 | |
|---|
| 1142 | /** |
|---|
| 1143 | * @return The projection to be used in calculating stuff. |
|---|
| 1144 | */ |
|---|
| 1145 | public Projection getProjection() { |
|---|
| 1146 | return Main.getProjection(); |
|---|
| 1147 | } |
|---|
| 1148 | |
|---|
| 1149 | public String helpTopic() { |
|---|
| 1150 | String n = getClass().getName(); |
|---|
| 1151 | return n.substring(n.lastIndexOf('.')+1); |
|---|
| 1152 | } |
|---|
| 1153 | |
|---|
| 1154 | /** |
|---|
| 1155 | * Return a ID which is unique as long as viewport dimensions are the same |
|---|
| 1156 | */ |
|---|
| 1157 | public int getViewID() { |
|---|
| 1158 | String x = center.east() + "_" + center.north() + "_" + scale + "_" + |
|---|
| 1159 | getWidth() + "_" + getHeight() + "_" + getProjection().toString(); |
|---|
| 1160 | java.util.zip.CRC32 id = new java.util.zip.CRC32(); |
|---|
| 1161 | id.update(x.getBytes()); |
|---|
| 1162 | return (int)id.getValue(); |
|---|
| 1163 | } |
|---|
| 1164 | |
|---|
| 1165 | public static SystemOfMeasurement getSystemOfMeasurement() { |
|---|
| 1166 | SystemOfMeasurement som = SYSTEMS_OF_MEASUREMENT.get(ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get()); |
|---|
| 1167 | if (som == null) |
|---|
| 1168 | return METRIC_SOM; |
|---|
| 1169 | return som; |
|---|
| 1170 | } |
|---|
| 1171 | |
|---|
| 1172 | public static class SystemOfMeasurement { |
|---|
| 1173 | public final double aValue; |
|---|
| 1174 | public final double bValue; |
|---|
| 1175 | public final String aName; |
|---|
| 1176 | public final String bName; |
|---|
| 1177 | |
|---|
| 1178 | /** |
|---|
| 1179 | * System of measurement. Currently covers only length units. |
|---|
| 1180 | * |
|---|
| 1181 | * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as |
|---|
| 1182 | * x_a == x_m / aValue |
|---|
| 1183 | */ |
|---|
| 1184 | public SystemOfMeasurement(double aValue, String aName, double bValue, String bName) { |
|---|
| 1185 | this.aValue = aValue; |
|---|
| 1186 | this.aName = aName; |
|---|
| 1187 | this.bValue = bValue; |
|---|
| 1188 | this.bName = bName; |
|---|
| 1189 | } |
|---|
| 1190 | |
|---|
| 1191 | public String getDistText(double dist) { |
|---|
| 1192 | double a = dist / aValue; |
|---|
| 1193 | if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && a > bValue / aValue) { |
|---|
| 1194 | double b = dist / bValue; |
|---|
| 1195 | return String.format(Locale.US, "%." + (b<10 ? 2 : 1) + "f %s", b, bName); |
|---|
| 1196 | } else if (a < 0.01) |
|---|
| 1197 | return "< 0.01 " + aName; |
|---|
| 1198 | else |
|---|
| 1199 | return String.format(Locale.US, "%." + (a<10 ? 2 : 1) + "f %s", a, aName); |
|---|
| 1200 | } |
|---|
| 1201 | } |
|---|
| 1202 | |
|---|
| 1203 | public static final SystemOfMeasurement METRIC_SOM = new SystemOfMeasurement(1, "m", 1000, "km"); |
|---|
| 1204 | public static final SystemOfMeasurement CHINESE_SOM = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */); |
|---|
| 1205 | public static final SystemOfMeasurement IMPERIAL_SOM = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi"); |
|---|
| 1206 | |
|---|
| 1207 | public static final Map<String, SystemOfMeasurement> SYSTEMS_OF_MEASUREMENT; |
|---|
| 1208 | static { |
|---|
| 1209 | SYSTEMS_OF_MEASUREMENT = new LinkedHashMap<String, SystemOfMeasurement>(); |
|---|
| 1210 | SYSTEMS_OF_MEASUREMENT.put(marktr("Metric"), METRIC_SOM); |
|---|
| 1211 | SYSTEMS_OF_MEASUREMENT.put(marktr("Chinese"), CHINESE_SOM); |
|---|
| 1212 | SYSTEMS_OF_MEASUREMENT.put(marktr("Imperial"), IMPERIAL_SOM); |
|---|
| 1213 | } |
|---|
| 1214 | |
|---|
| 1215 | private static class CursorInfo { |
|---|
| 1216 | public Cursor cursor; |
|---|
| 1217 | public Object object; |
|---|
| 1218 | public CursorInfo(Cursor c, Object o) { |
|---|
| 1219 | cursor = c; |
|---|
| 1220 | object = o; |
|---|
| 1221 | } |
|---|
| 1222 | } |
|---|
| 1223 | |
|---|
| 1224 | private LinkedList<CursorInfo> Cursors = new LinkedList<CursorInfo>(); |
|---|
| 1225 | /** |
|---|
| 1226 | * Set new cursor. |
|---|
| 1227 | */ |
|---|
| 1228 | public void setNewCursor(Cursor cursor, Object reference) { |
|---|
| 1229 | if(Cursors.size() > 0) { |
|---|
| 1230 | CursorInfo l = Cursors.getLast(); |
|---|
| 1231 | if(l != null && l.cursor == cursor && l.object == reference) |
|---|
| 1232 | return; |
|---|
| 1233 | stripCursors(reference); |
|---|
| 1234 | } |
|---|
| 1235 | Cursors.add(new CursorInfo(cursor, reference)); |
|---|
| 1236 | setCursor(cursor); |
|---|
| 1237 | } |
|---|
| 1238 | public void setNewCursor(int cursor, Object reference) { |
|---|
| 1239 | setNewCursor(Cursor.getPredefinedCursor(cursor), reference); |
|---|
| 1240 | } |
|---|
| 1241 | /** |
|---|
| 1242 | * Remove the new cursor and reset to previous |
|---|
| 1243 | */ |
|---|
| 1244 | public void resetCursor(Object reference) { |
|---|
| 1245 | if(Cursors.size() == 0) { |
|---|
| 1246 | setCursor(null); |
|---|
| 1247 | return; |
|---|
| 1248 | } |
|---|
| 1249 | CursorInfo l = Cursors.getLast(); |
|---|
| 1250 | stripCursors(reference); |
|---|
| 1251 | if(l != null && l.object == reference) { |
|---|
| 1252 | if(Cursors.size() == 0) { |
|---|
| 1253 | setCursor(null); |
|---|
| 1254 | } else { |
|---|
| 1255 | setCursor(Cursors.getLast().cursor); |
|---|
| 1256 | } |
|---|
| 1257 | } |
|---|
| 1258 | } |
|---|
| 1259 | |
|---|
| 1260 | private void stripCursors(Object reference) { |
|---|
| 1261 | LinkedList<CursorInfo> c = new LinkedList<CursorInfo>(); |
|---|
| 1262 | for(CursorInfo i : Cursors) { |
|---|
| 1263 | if(i.object != reference) { |
|---|
| 1264 | c.add(i); |
|---|
| 1265 | } |
|---|
| 1266 | } |
|---|
| 1267 | Cursors = c; |
|---|
| 1268 | } |
|---|
| 1269 | } |
|---|