Ticket #11637: 0011-Moved-zoom-and-center-management-and-coordinate-conv.patch
| File 0011-Moved-zoom-and-center-management-and-coordinate-conv.patch, 73.4 KB (added by , 11 years ago) |
|---|
-
src/org/openstreetmap/josm/gui/MapMover.java
From 5b10b5e833302743527049a9ab888cf0a55cbf7f Mon Sep 17 00:00:00 2001 From: Michael Zangl <michael.zangl@student.kit.edu> Date: Wed, 1 Jul 2015 17:22:24 +0200 Subject: [PATCH 11/11] Moved zoom and center management and coordinate conversion of MapView to a new class. --- src/org/openstreetmap/josm/gui/MapMover.java | 89 ++- src/org/openstreetmap/josm/gui/MapStatus.java | 4 +- src/org/openstreetmap/josm/gui/MapView.java | 27 +- .../josm/gui/NavigatableComponent.java | 344 ++++------ .../josm/gui/navigate/NavigationModel.java | 730 +++++++++++++++++++++ test/unit/org/openstreetmap/josm/TestUtils.java | 36 + .../josm/gui/NavigatableComponentTest.java | 187 ++++++ 7 files changed, 1158 insertions(+), 259 deletions(-) create mode 100644 src/org/openstreetmap/josm/gui/navigate/NavigationModel.java create mode 100644 test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java diff --git a/src/org/openstreetmap/josm/gui/MapMover.java b/src/org/openstreetmap/josm/gui/MapMover.java index d8ea39a..943cd04 100644
a b package org.openstreetmap.josm.gui; 3 3 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.Component; 6 7 import java.awt.Cursor; 7 8 import java.awt.Point; 8 9 import java.awt.event.ActionEvent; … … import java.awt.event.MouseEvent; 12 13 import java.awt.event.MouseMotionListener; 13 14 import java.awt.event.MouseWheelEvent; 14 15 import java.awt.event.MouseWheelListener; 16 import java.awt.geom.Point2D; 15 17 16 18 import javax.swing.AbstractAction; 17 19 import javax.swing.ActionMap; … … import javax.swing.KeyStroke; 23 25 import org.openstreetmap.josm.Main; 24 26 import org.openstreetmap.josm.actions.mapmode.SelectAction; 25 27 import org.openstreetmap.josm.data.coor.EastNorth; 28 import org.openstreetmap.josm.gui.navigate.NavigationCursorManager; 29 import org.openstreetmap.josm.gui.navigate.NavigationModel; 30 import org.openstreetmap.josm.gui.navigate.NavigationModel.ScrollMode; 26 31 import org.openstreetmap.josm.tools.Destroyable; 27 32 import org.openstreetmap.josm.tools.Shortcut; 28 33 … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 44 49 @Override 45 50 public void actionPerformed(ActionEvent e) { 46 51 if (".".equals(action) || ",".equals(action)) { 47 Point mouse = nc.getMousePosition();52 Point2D mouse = lastMousePosition; 48 53 if (mouse == null) 49 mouse = new Point((int) nc.getBounds().getCenterX(), (int) nc.getBounds().getCenterY()); 50 MouseWheelEvent we = new MouseWheelEvent(nc, e.getID(), e.getWhen(), e.getModifiers(), mouse.x, mouse.y, 0, false, 54 mouse = nm.getScreenPosition(nm.getCenter()); 55 MouseWheelEvent we = new MouseWheelEvent((Component) e.getSource(), e.getID(), e.getWhen(), e.getModifiers(), (int) mouse.getX(), (int) mouse.getY(), 0, false, 56 51 57 MouseWheelEvent.WHEEL_UNIT_SCROLL, 1, ",".equals(action) ? -1 : 1); 52 58 mouseWheelMoved(we); 53 59 } else { 54 EastNorth center = nc.getCenter();55 EastNorth newcenter = nc.getEastNorth(nc.getWidth()/2+nc.getWidth()/5, nc.getHeight()/2+nc.getHeight()/5);60 double relativeX = .5; 61 double relativeY = .5; 56 62 switch(action) { 57 63 case "left": 58 nc.zoomTo(new EastNorth(2*center.east()-newcenter.east(), center.north()));64 relativeX -= .2; 59 65 break; 60 66 case "right": 61 nc.zoomTo(new EastNorth(newcenter.east(), center.north()));67 relativeX += .2; 62 68 break; 63 69 case "up": 64 nc.zoomTo(new EastNorth(center.east(), 2*center.north()-newcenter.north()));70 relativeY -= .2; 65 71 break; 66 72 case "down": 67 nc.zoomTo(new EastNorth(center.east(), newcenter.north()));73 relativeY += .2; 68 74 break; 69 75 } 76 EastNorth newcenter = nm.getEastNorthRelative(relativeX, relativeY); 77 nm.zoomTo(newcenter, ScrollMode.IMMEDIATE); 70 78 } 71 79 } 72 80 } … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 79 87 /** 80 88 * The map to move around. 81 89 */ 82 private final Navigat ableComponent nc;90 private final NavigationModel nm; 83 91 private final JPanel contentPane; 84 92 85 93 private boolean movementInPlace = false; 94 private final NavigationCursorManager cursorManager; 95 96 private Point lastMousePosition = null; 86 97 87 98 /** 88 99 * Constructs a new {@code MapMover}. 89 * @param navComp the navigatable component 100 * @param navigationModel the navigatable component 101 * @param cursorManager A cursor manager to which we should send cursor changes. 90 102 * @param contentPane the content pane 91 103 */ 92 public MapMover(NavigatableComponent navComp, JPanel contentPane) { 93 this.nc = navComp; 104 public MapMover(NavigationModel navigationModel, NavigationCursorManager cursorManager, JPanel contentPane) { 105 this.nm = navigationModel; 106 this.cursorManager = cursorManager; 94 107 this.contentPane = contentPane; 95 nc.addMouseListener(this);96 nc.addMouseMotionListener(this);97 nc.addMouseWheelListener(this);98 108 99 109 if (contentPane != null) { 100 110 // CHECKSTYLE.OFF: LineLength … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 137 147 } 138 148 139 149 /** 150 * Registers the mouse events of a component so that they move the map on the right actions. 151 * @param c The component to register the event on. 152 */ 153 public void registerMouseEvents(Component c) { 154 c.addMouseListener(this); 155 c.addMouseMotionListener(this); 156 c.addMouseWheelListener(this); 157 } 158 159 /** 140 160 * If the right (and only the right) mouse button is pressed, move the map. 141 161 */ 142 162 @Override … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 150 170 if (stdMovement || (macMovement && allowedMode)) { 151 171 if (mousePosMove == null) 152 172 startMovement(e); 153 EastNorth center = n c.getCenter();154 EastNorth mouseCenter = n c.getEastNorth(e.getX(), e.getY());155 n c.zoomTo(new EastNorth(173 EastNorth center = nm.getCenter(); 174 EastNorth mouseCenter = nm.getEastNorth(e.getPoint()); 175 nm.zoomTo(new EastNorth( 156 176 mousePosMove.east() + center.east() - mouseCenter.east(), 157 mousePosMove.north() + center.north() - mouseCenter.north()) );177 mousePosMove.north() + center.north() - mouseCenter.north()), ScrollMode.IMMEDIATE); 158 178 } else { 159 179 endMovement(); 160 180 } 181 updateMousePosition(e); 161 182 } 162 183 163 184 /** … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 171 192 Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask) { 172 193 startMovement(e); 173 194 } 195 updateMousePosition(e); 174 196 } 175 197 176 198 /** … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 181 203 if (e.getButton() == MouseEvent.BUTTON3 || Main.isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1) { 182 204 endMovement(); 183 205 } 206 updateMousePosition(e); 207 } 208 209 @Override 210 public void mouseExited(MouseEvent e) { 211 lastMousePosition = null; 184 212 } 185 213 186 214 /** … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 192 220 if (movementInPlace) 193 221 return; 194 222 movementInPlace = true; 195 mousePosMove = n c.getEastNorth(e.getX(), e.getY());196 nc.setNewCursor(Cursor.MOVE_CURSOR, this);223 mousePosMove = nm.getEastNorth(e.getX(), e.getY()); 224 cursorManager.setNewCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR), this); 197 225 } 198 226 199 227 /** … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 203 231 if (!movementInPlace) 204 232 return; 205 233 movementInPlace = false; 206 nc.resetCursor(this);234 cursorManager.resetCursor(this); 207 235 mousePosMove = null; 208 236 } 209 237 … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 213 241 */ 214 242 @Override 215 243 public void mouseWheelMoved(MouseWheelEvent e) { 216 n c.zoomToFactor(e.getX(), e.getY(), Math.pow(Math.sqrt(2), e.getWheelRotation()));244 nm.zoomToFactorAround(e.getPoint(), Math.pow(Math.sqrt(2), e.getWheelRotation())); 217 245 } 218 246 219 247 /** … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 230 258 if (mousePosMove == null) { 231 259 startMovement(e); 232 260 } 233 EastNorth center = n c.getCenter();234 EastNorth mouseCenter = n c.getEastNorth(e.getX(), e.getY());235 n c.zoomTo(new EastNorth(mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north()236 + center.north() - mouseCenter.north()) );261 EastNorth center = nm.getCenter(); 262 EastNorth mouseCenter = nm.getEastNorth(e.getX(), e.getY()); 263 nm.zoomTo(new EastNorth(mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north() 264 + center.north() - mouseCenter.north()), ScrollMode.IMMEDIATE); 237 265 } else { 238 266 endMovement(); 239 267 } 240 268 } 269 updateMousePosition(e); 241 270 } 242 271 243 272 @Override … … public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse 264 293 } 265 294 } 266 295 } 296 297 private void updateMousePosition(MouseEvent e) { 298 lastMousePosition = e.getPoint(); 299 } 267 300 } -
src/org/openstreetmap/josm/gui/MapStatus.java
diff --git a/src/org/openstreetmap/josm/gui/MapStatus.java b/src/org/openstreetmap/josm/gui/MapStatus.java index 51e87e2..9cc71bb 100644
a b public class MapStatus extends JPanel implements Helpful, Destroyable, Preferenc 376 376 return; // exit, if new parent. 377 377 378 378 // Do nothing, if required data is missing 379 if (ms.mousePos == null || mv. center== null) {379 if (ms.mousePos == null || mv.getCenter() == null) { 380 380 continue; 381 381 } 382 382 … … public class MapStatus extends JPanel implements Helpful, Destroyable, Preferenc 822 822 823 823 @Override 824 824 public void mouseMoved(MouseEvent e) { 825 if (mv. center== null)825 if (mv.getCenter() == null) 826 826 return; 827 827 // Do not update the view if ctrl is pressed. 828 828 if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0) { -
src/org/openstreetmap/josm/gui/MapView.java
diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java index 77613b4..c9b80f4 100644
a b import java.util.Arrays; 26 26 import java.util.Collection; 27 27 import java.util.Collections; 28 28 import java.util.LinkedHashSet; 29 import java.util.LinkedList; 29 30 import java.util.List; 30 31 import java.util.ListIterator; 31 32 import java.util.Set; … … import org.openstreetmap.josm.gui.layer.OsmDataLayer; 61 62 import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer; 62 63 import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 63 64 import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker; 65 import org.openstreetmap.josm.gui.navigate.NavigationModel; 66 import org.openstreetmap.josm.gui.navigate.NavigationModel.ZoomData; 64 67 import org.openstreetmap.josm.gui.util.GuiHelper; 65 68 import org.openstreetmap.josm.tools.AudioPlayer; 66 69 import org.openstreetmap.josm.tools.BugReportExceptionHandler; … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 279 282 // Layers that wasn't changed since last paint 280 283 private final transient List<Layer> nonChangedLayers = new ArrayList<>(); 281 284 private transient Layer changedLayer; 282 private int lastViewID;283 285 private boolean paintPreferencesChanged = true; 284 286 private Rectangle lastClipBounds = new Rectangle(); 285 287 private transient MapMover mapMover; … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 304 306 MapView.this.add(c); 305 307 } 306 308 307 mapMover = new MapMover(MapView.this, contentPane); 309 mapMover = new MapMover(getNavigationModel(), cursorManager, contentPane); 310 mapMover.registerMouseEvents(MapView.this); 311 // Notify the map view that it has changed. 312 repaint(); 308 313 } 309 314 }); 310 315 … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 331 336 } 332 337 }); 333 338 334 if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0) !=null) {339 if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null) { 335 340 setFocusTraversalKeysEnabled(false); 336 341 } 337 342 } … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 727 732 canUseBuffer = !paintPreferencesChanged; 728 733 paintPreferencesChanged = false; 729 734 } 730 canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount && 731 lastViewID == getViewID() && lastClipBounds.contains(g.getClipBounds()); 735 canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount && lastClipBounds.contains(g.getClipBounds()); 732 736 if (canUseBuffer) { 733 737 for (int i = 0; i < nonChangedLayers.size(); i++) { 734 738 if (visibleLayers.get(i) != nonChangedLayers.get(i)) { … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 775 779 for (int i = 0; i < nonChangedLayersCount; i++) { 776 780 nonChangedLayers.add(visibleLayers.get(i)); 777 781 } 778 lastViewID = getViewID();779 782 lastClipBounds = g.getClipBounds(); 780 783 781 784 tempG.drawImage(nonChangedLayersBuffer, 0, 0, null); … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 1019 1022 if (layer == activeLayer) 1020 1023 return false; 1021 1024 1022 Layer old = activeLayer;1023 1025 activeLayer = layer; 1024 1026 if (setEditLayer) { 1025 1027 setEditLayer(layers); … … implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer 1175 1177 } 1176 1178 } 1177 1179 1180 @Override 1181 public void zoomChanged(NavigationModel navigationModel, ZoomData oldZoom, ZoomData newZoom) { 1182 super.zoomChanged(navigationModel, oldZoom, newZoom); 1183 synchronized (this) { 1184 paintPreferencesChanged = true; 1185 } 1186 } 1187 1188 /** 1189 * A selection listener that fires a repaint as soon as the selection changes. 1190 */ 1178 1191 private transient SelectionChangedListener repaintSelectionChangedListener = new SelectionChangedListener() { 1179 1192 @Override 1180 1193 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { -
src/org/openstreetmap/josm/gui/NavigatableComponent.java
diff --git a/src/org/openstreetmap/josm/gui/NavigatableComponent.java b/src/org/openstreetmap/josm/gui/NavigatableComponent.java index 6bd0808..a8e6eb2 100644
a b import org.openstreetmap.josm.gui.help.Helpful; 52 52 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 53 53 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 54 54 import org.openstreetmap.josm.gui.navigate.NavigationCursorManager; 55 import org.openstreetmap.josm.gui.navigate.NavigationModel; 56 import org.openstreetmap.josm.gui.navigate.NavigationModel.ScrollMode; 57 import org.openstreetmap.josm.gui.navigate.NavigationModel.WeakZoomChangeListener; 58 import org.openstreetmap.josm.gui.navigate.NavigationModel.ZoomData; 55 59 import org.openstreetmap.josm.tools.Predicate; 56 60 import org.openstreetmap.josm.tools.Utils; 57 61 … … import org.openstreetmap.josm.tools.Utils; 62 66 * @author imi 63 67 * @since 41 64 68 */ 65 public class NavigatableComponent extends JComponent implements Helpful {69 public class NavigatableComponent extends JComponent implements Helpful, NavigationModel.ZoomChangeListener { 66 70 67 71 /** 68 72 * Interface to notify listeners of the change of the zoom area. … … public class NavigatableComponent extends JComponent implements Helpful { 74 78 void zoomChanged(); 75 79 } 76 80 81 private static final class ZoomChangeAdapter implements NavigationModel.ZoomChangeListener { 82 83 private ZoomChangeListener listener; 84 85 public ZoomChangeAdapter(ZoomChangeListener listener) { 86 this.listener = listener; 87 } 88 89 @Override 90 public void zoomChanged(NavigationModel navigationModel, ZoomData oldZoom, ZoomData newZoom) { 91 listener.zoomChanged(); 92 } 93 94 @Override 95 public int hashCode() { 96 final int prime = 31; 97 int result = 1; 98 result = prime * result + ((listener == null) ? 0 : listener.hashCode()); 99 return result; 100 } 101 102 @Override 103 public boolean equals(Object obj) { 104 if (this == obj) 105 return true; 106 if (obj == null) 107 return false; 108 if (getClass() != obj.getClass()) 109 return false; 110 ZoomChangeAdapter other = (ZoomChangeAdapter) obj; 111 if (listener == null) { 112 if (other.listener != null) 113 return false; 114 } else if (!listener.equals(other.listener)) 115 return false; 116 return true; 117 } 118 } 119 77 120 /** 78 121 * Interface to notify listeners of the change of the system of measurement. 79 122 * @since 6056 … … public class NavigatableComponent extends JComponent implements Helpful { 101 144 public static final String PROPNAME_SCALE = "scale"; 102 145 103 146 /** 104 * the zoom listeners 147 * This is the navigation model for the one single map view. 148 * Due to backwards compatibility (zoom change listeners, ...), we use a static field here. 105 149 */ 106 private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();150 private static final NavigationModel defaultNavigationModel = new NavigationModel(); 107 151 108 152 /** 109 153 * Removes a zoom change listener … … public class NavigatableComponent extends JComponent implements Helpful { 111 155 * @param listener the listener. Ignored if null or already absent 112 156 */ 113 157 public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) { 114 zoomChangeListeners.remove(listener);158 defaultNavigationModel.removeZoomChangeListener(new ZoomChangeAdapter(listener)); 115 159 } 116 160 117 161 /** … … public class NavigatableComponent extends JComponent implements Helpful { 120 164 * @param listener the listener. Ignored if null or already registered. 121 165 */ 122 166 public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) { 123 if (listener != null) { 124 zoomChangeListeners.addIfAbsent(listener); 125 } 126 } 127 128 protected static void fireZoomChanged() { 129 for (ZoomChangeListener l : zoomChangeListeners) { 130 l.zoomChanged(); 131 } 167 defaultNavigationModel.addZoomChangeListener(new ZoomChangeAdapter(listener)); 132 168 } 133 169 134 135 170 /** 136 171 * Removes a SoM change listener 137 172 * … … public class NavigatableComponent extends JComponent implements Helpful { 175 210 SystemOfMeasurement.setSystemOfMeasurement(somKey); 176 211 } 177 212 178 private double scale = Main.getProjection().getDefaultZoomInPPD();179 /**180 * Center n/e coordinate of the desired screen center.181 */182 protected EastNorth center = calculateDefaultCenter();183 213 184 214 private final transient Object paintRequestLock = new Object(); 185 215 private Rectangle paintRect = null; … … public class NavigatableComponent extends JComponent implements Helpful { 187 217 188 218 protected transient ViewportData initialViewport; 189 219 220 private final NavigationModel navigationModel; 221 222 private transient final NavigationModel.ZoomChangeListener weakZoomListener = new WeakZoomChangeListener(this); 223 190 224 protected transient final NavigationCursorManager cursorManager = new NavigationCursorManager(this); 191 225 192 226 /** 193 * Constructs a new {@code NavigatableComponent} .227 * Constructs a new {@code NavigatableComponent} using the static default {@link NavigationModel} and zooming to the current bounds, 194 228 */ 195 229 public NavigatableComponent() { 230 this(defaultNavigationModel); 231 defaultNavigationModel.zoomTo( 232 calculateDefaultCenter(), 233 Main.getProjection().getDefaultZoomInPPD() 234 ); 235 } 236 237 /** 238 * Constructs a new {@code NavigatableComponent} 239 * @param navigationModel The navigation model to use. 240 */ 241 public NavigatableComponent(NavigationModel navigationModel) { 242 this.navigationModel = navigationModel; 196 243 setLayout(null); 244 navigationModel.addZoomChangeListener(weakZoomListener); 245 navigationModel.trackComponentSize(this); 246 } 247 248 /** 249 * Gets the navigation model that is used to convert between screen and world coordinates and handles zooming. 250 * @return The navigation model this component was constructed with. 251 */ 252 public NavigationModel getNavigationModel() { 253 return navigationModel; 197 254 } 198 255 199 256 protected DataSet getCurrentDataSet() { … … public class NavigatableComponent extends JComponent implements Helpful { 257 314 } 258 315 259 316 public double getDist100Pixel() { 260 int w = getWidth()/2; 261 int h = getHeight()/2; 262 LatLon ll1 = getLatLon(w-50, h); 263 LatLon ll2 = getLatLon(w+50, h); 264 return ll1.greatCircleDistance(ll2); 317 return navigationModel.getPixelDistance(100); 265 318 } 266 319 267 320 /** … … public class NavigatableComponent extends JComponent implements Helpful { 269 322 * change the center by accessing the return value. Use zoomTo instead. 270 323 */ 271 324 public EastNorth getCenter() { 272 return center;325 return navigationModel.getCenter(); 273 326 } 274 327 275 328 public double getScale() { 276 return scale;329 return navigationModel.getScale(); 277 330 } 278 331 279 332 /** … … public class NavigatableComponent extends JComponent implements Helpful { 283 336 * @return Geographic coordinates from a specific pixel coordination on the screen. 284 337 */ 285 338 public EastNorth getEastNorth(int x, int y) { 286 return new EastNorth( 287 center.east() + (x - getWidth()/2.0)*scale, 288 center.north() - (y - getHeight()/2.0)*scale); 339 return navigationModel.getEastNorth(x, y); 289 340 } 290 341 291 342 public ProjectionBounds getProjectionBounds() { 292 return new ProjectionBounds( 293 new EastNorth( 294 center.east() - getWidth()/2.0*scale, 295 center.north() - getHeight()/2.0*scale), 296 new EastNorth( 297 center.east() + getWidth()/2.0*scale, 298 center.north() + getHeight()/2.0*scale)); 343 return new ProjectionBounds(getEastNorth(0, getHeight()), getEastNorth(getWidth(), 0)); 299 344 } 300 345 301 346 /* FIXME: replace with better method - used by MapSlider */ … … public class NavigatableComponent extends JComponent implements Helpful { 308 353 /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */ 309 354 public Bounds getRealBounds() { 310 355 return new Bounds( 311 getProjection().eastNorth2latlon(new EastNorth( 312 center.east() - getWidth()/2.0*scale, 313 center.north() - getHeight()/2.0*scale)), 314 getProjection().eastNorth2latlon(new EastNorth( 315 center.east() + getWidth()/2.0*scale, 316 center.north() + getHeight()/2.0*scale))); 356 getProjection().eastNorth2latlon(getEastNorth(0, getHeight())), 357 getProjection().eastNorth2latlon(getEastNorth(getWidth(), 0))); 317 358 } 318 359 319 360 /** … … public class NavigatableComponent extends JComponent implements Helpful { 324 365 * on the screen. 325 366 */ 326 367 public LatLon getLatLon(int x, int y) { 327 return getProjection().eastNorth2latlon(getEastNorth(x, y));368 return navigationModel.getLatLon(new Point2D.Double(x, y)); 328 369 } 329 370 330 371 public LatLon getLatLon(double x, double y) { 331 return getLatLon((int) x, (int) y);372 return navigationModel.getLatLon(new Point2D.Double(x, y)); 332 373 } 333 374 334 375 /** … … public class NavigatableComponent extends JComponent implements Helpful { 350 391 double deltaNorth = (northMax - northMin) / 10; 351 392 352 393 for (int i = 0; i < 10; i++) { 353 result.extend( Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin)));354 result.extend( Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax)));355 result.extend( Main.getProjection().eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth)));356 result.extend( Main.getProjection().eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth)));394 result.extend(getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin))); 395 result.extend(getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax))); 396 result.extend(getProjection().eastNorth2latlon(new EastNorth(eastMin, northMin + i * deltaNorth))); 397 result.extend(getProjection().eastNorth2latlon(new EastNorth(eastMax, northMin + i * deltaNorth))); 357 398 } 358 399 359 400 return result; 360 401 } 361 402 362 403 public AffineTransform getAffineTransform() { 363 return new AffineTransform( 364 1.0/scale, 0.0, 0.0, -1.0/scale, getWidth()/2.0 - center.east()/scale, getHeight()/2.0 + center.north()/scale); 404 return navigationModel.getAffineTransform(); 365 405 } 366 406 367 407 /** … … public class NavigatableComponent extends JComponent implements Helpful { 371 411 * to the own top/left. 372 412 */ 373 413 public Point2D getPoint2D(EastNorth p) { 374 if (null == p) 375 return new Point(); 376 double x = (p.east()-center.east())/scale + getWidth()/2d; 377 double y = (center.north()-p.north())/scale + getHeight()/2d; 378 return new Point2D.Double(x, y); 414 return navigationModel.getScreenPosition(p); 379 415 } 380 416 381 417 public Point2D getPoint2D(LatLon latlon) { 382 if (latlon == null) 383 return new Point(); 384 else if (latlon instanceof CachedLatLon) 385 return getPoint2D(((CachedLatLon) latlon).getEastNorth()); 386 else 387 return getPoint2D(getProjection().latlon2eastNorth(latlon)); 418 return navigationModel.getScreenPosition(latlon); 388 419 } 389 420 390 421 public Point2D getPoint2D(Node n) { … … public class NavigatableComponent extends JComponent implements Helpful { 430 461 * @param initial true if this call initializes the viewport. 431 462 */ 432 463 public void zoomTo(EastNorth newCenter, double newScale, boolean initial) { 433 Bounds b = getProjection().getWorldBoundsLatLon(); 434 LatLon cl = Projections.inverseProject(newCenter); 435 boolean changed = false; 436 double lat = cl.lat(); 437 double lon = cl.lon(); 438 if (lat < b.getMinLat()) { 439 changed = true; 440 lat = b.getMinLat(); 441 } else if (lat > b.getMaxLat()) { 442 changed = true; 443 lat = b.getMaxLat(); 444 } 445 if (lon < b.getMinLon()) { 446 changed = true; 447 lon = b.getMinLon(); 448 } else if (lon > b.getMaxLon()) { 449 changed = true; 450 lon = b.getMaxLon(); 451 } 452 if (changed) { 453 newCenter = Projections.project(new LatLon(lat, lon)); 454 } 455 int width = getWidth()/2; 456 int height = getHeight()/2; 457 LatLon l1 = new LatLon(b.getMinLat(), lon); 458 LatLon l2 = new LatLon(b.getMaxLat(), lon); 459 EastNorth e1 = getProjection().latlon2eastNorth(l1); 460 EastNorth e2 = getProjection().latlon2eastNorth(l2); 461 double d = e2.north() - e1.north(); 462 if (height > 0 && d < height*newScale) { 463 double newScaleH = d/height; 464 e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMinLon())); 465 e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMaxLon())); 466 d = e2.east() - e1.east(); 467 if (width > 0 && d < width*newScale) { 468 newScale = Math.max(newScaleH, d/width); 469 } 470 } else if (height > 0) { 471 d = d/(l1.greatCircleDistance(l2)*height*10); 472 if (newScale < d) { 473 newScale = d; 474 } 475 } 476 477 if (!newCenter.equals(center) || !Utils.equalsEpsilon(scale, newScale)) { 478 if (!initial) { 479 pushZoomUndo(center, scale); 480 } 481 zoomNoUndoTo(newCenter, newScale, initial); 482 } 483 } 484 485 /** 486 * Zoom to the given coordinate without adding to the zoom undo buffer. 487 * 488 * @param newCenter The center x-value (easting) to zoom to. 489 * @param newScale The scale to use. 490 * @param initial true if this call initializes the viewport. 491 */ 492 private void zoomNoUndoTo(EastNorth newCenter, double newScale, boolean initial) { 493 if (!newCenter.equals(center)) { 494 EastNorth oldCenter = center; 495 center = newCenter; 496 if (!initial) { 497 firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter); 498 } 499 } 500 if (!Utils.equalsEpsilon(scale, newScale)) { 501 double oldScale = scale; 502 scale = newScale; 503 if (!initial) { 504 firePropertyChange(PROPNAME_SCALE, oldScale, newScale); 505 } 506 } 507 508 if (!initial) { 509 repaint(); 510 fireZoomChanged(); 511 } 464 navigationModel.zoomTo(newCenter, newScale, initial ? ScrollMode.INITIAL : ScrollMode.DEFAULT); 512 465 } 513 466 514 467 public void zoomTo(EastNorth newCenter) { 515 zoomTo(newCenter, scale);468 zoomTo(newCenter, navigationModel.getScale()); 516 469 } 517 470 518 471 public void zoomTo(LatLon newCenter) { … … public class NavigatableComponent extends JComponent implements Helpful { 529 482 */ 530 483 public void smoothScrollTo(EastNorth newCenter) { 531 484 // FIXME make these configurable. 532 final int fps = 20; // animation frames per second 533 final int speed = 1500; // milliseconds for full-screen-width pan 534 if (!newCenter.equals(center)) { 535 final EastNorth oldCenter = center; 536 final double distance = newCenter.distance(oldCenter) / scale; 537 final double milliseconds = distance / getWidth() * speed; 538 final double frames = milliseconds * fps / 1000; 539 final EastNorth finalNewCenter = newCenter; 540 541 new Thread() { 542 @Override 543 public void run() { 544 for (int i = 0; i < frames; i++) { 545 // FIXME - not use zoom history here 546 zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames)); 547 try { 548 Thread.sleep(1000 / fps); 549 } catch (InterruptedException ex) { 550 Main.warn("InterruptedException in "+NavigatableComponent.class.getSimpleName()+" during smooth scrolling"); 551 } 552 } 553 } 554 }.start(); 555 } 485 navigationModel.zoomTo(newCenter, ScrollMode.ANIMATE); 556 486 } 557 487 558 488 public void zoomToFactor(double x, double y, double factor) { 559 double newScale = scale*factor; 560 // New center position so that point under the mouse pointer stays the same place as it was before zooming 561 // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom 562 zoomTo(new EastNorth( 563 center.east() - (x - getWidth()/2.0) * (newScale - scale), 564 center.north() + (y - getHeight()/2.0) * (newScale - scale)), 565 newScale); 489 navigationModel.zoomToFactorAround(new Point2D.Double(x, y), factor); 566 490 } 567 491 568 492 public void zoomToFactor(EastNorth newCenter, double factor) { 569 zoomTo(newCenter, scale*factor);493 zoomTo(newCenter, getScale()*factor); 570 494 } 571 495 572 496 public void zoomToFactor(double factor) { 573 zoomTo( center, scale*factor);497 zoomTo(getCenter(), getScale()*factor); 574 498 } 575 499 576 500 public void zoomTo(ProjectionBounds box) { … … public class NavigatableComponent extends JComponent implements Helpful { 624 548 zoomTo(box.getBounds()); 625 549 } 626 550 627 private class ZoomData {628 private final LatLon center;629 private final double scale;630 631 public ZoomData(EastNorth center, double scale) {632 this.center = Projections.inverseProject(center);633 this.scale = scale;634 }635 636 public EastNorth getCenterEastNorth() {637 return getProjection().latlon2eastNorth(center);638 }639 640 public double getScale() {641 return scale;642 }643 }644 645 private Stack<ZoomData> zoomUndoBuffer = new Stack<>();646 private Stack<ZoomData> zoomRedoBuffer = new Stack<>();647 private Date zoomTimestamp = new Date();648 649 private void pushZoomUndo(EastNorth center, double scale) {650 Date now = new Date();651 if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {652 zoomUndoBuffer.push(new ZoomData(center, scale));653 if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {654 zoomUndoBuffer.remove(0);655 }656 zoomRedoBuffer.clear();657 }658 zoomTimestamp = now;659 }660 661 551 public void zoomPrevious() { 662 if (!zoomUndoBuffer.isEmpty()) { 663 ZoomData zoom = zoomUndoBuffer.pop(); 664 zoomRedoBuffer.push(new ZoomData(center, scale)); 665 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false); 666 } 552 navigationModel.zoomPrevious(); 667 553 } 668 554 669 555 public void zoomNext() { 670 if (!zoomRedoBuffer.isEmpty()) { 671 ZoomData zoom = zoomRedoBuffer.pop(); 672 zoomUndoBuffer.push(new ZoomData(center, scale)); 673 zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale(), false); 674 } 556 navigationModel.zoomNext(); 675 557 } 676 558 677 559 public boolean hasZoomUndoEntries() { 678 return !zoomUndoBuffer.isEmpty();560 return navigationModel.hasPreviousZoomEntries(); 679 561 } 680 562 681 563 public boolean hasZoomRedoEntries() { 682 return !zoomRedoBuffer.isEmpty();564 return navigationModel.hasNextZoomEntries(); 683 565 } 684 566 685 567 private BBox getBBox(Point p, int snapDistance) { … … public class NavigatableComponent extends JComponent implements Helpful { 1431 1313 * @return A unique ID, as long as viewport dimensions are the same 1432 1314 */ 1433 1315 public int getViewID() { 1434 String x = center.east() + "_" + center.north() + "_" + scale+ "_" +1316 String x = getCenter().east() + "_" + getCenter().north() + "_" + getScale() + "_" + 1435 1317 getWidth() + "_" + getHeight() + "_" + getProjection().toString(); 1436 1318 CRC32 id = new CRC32(); 1437 1319 id.update(x.getBytes(StandardCharsets.UTF_8)); … … public class NavigatableComponent extends JComponent implements Helpful { 1465 1347 } 1466 1348 1467 1349 @Override 1350 public void zoomChanged(NavigationModel navigationModel, ZoomData oldZoom, ZoomData newZoom) { 1351 if (oldZoom == null) { 1352 // initial. 1353 return; 1354 } 1355 EastNorth oldCenter = oldZoom.getCenterEastNorth(getProjection()); 1356 EastNorth newCenter = newZoom.getCenterEastNorth(getProjection()); 1357 if (!newCenter.equals(oldCenter)) { 1358 firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter); 1359 } 1360 double oldScale = oldZoom.getScale(); 1361 double newScale = newZoom.getScale(); 1362 if (!Utils.equalsEpsilon(oldScale, newScale)) { 1363 firePropertyChange(PROPNAME_SCALE, oldScale, newScale); 1364 } 1365 repaint(); 1366 } 1367 @Override 1468 1368 public void paint(Graphics g) { 1469 1369 synchronized (paintRequestLock) { 1470 1370 if (paintRect != null) { -
new file src/org/openstreetmap/josm/gui/navigate/NavigationModel.java
diff --git a/src/org/openstreetmap/josm/gui/navigate/NavigationModel.java b/src/org/openstreetmap/josm/gui/navigate/NavigationModel.java new file mode 100644 index 0000000..4ec3828
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.navigate; 3 4 import java.awt.Component; 5 import java.awt.Dimension; 6 import java.awt.Point; 7 import java.awt.event.ComponentAdapter; 8 import java.awt.event.ComponentEvent; 9 import java.awt.geom.AffineTransform; 10 import java.awt.geom.Point2D; 11 import java.lang.ref.WeakReference; 12 import java.util.Date; 13 import java.util.Stack; 14 import java.util.Timer; 15 import java.util.TimerTask; 16 import java.util.concurrent.CopyOnWriteArrayList; 17 18 import org.openstreetmap.josm.Main; 19 import org.openstreetmap.josm.data.Bounds; 20 import org.openstreetmap.josm.data.coor.CachedLatLon; 21 import org.openstreetmap.josm.data.coor.EastNorth; 22 import org.openstreetmap.josm.data.coor.LatLon; 23 import org.openstreetmap.josm.data.projection.Projection; 24 import org.openstreetmap.josm.data.projection.Projections; 25 import org.openstreetmap.josm.gui.util.GuiHelper; 26 import org.openstreetmap.josm.tools.Utils; 27 28 /** 29 * This class manages the current position on the map and provides utility methods to convert between view and NorthEast coordinates. There are convenience methods to directly convert to LatLon. 30 * 31 * @author Michael Zangl 32 * 33 */ 34 public class NavigationModel { 35 /** 36 * Interface to notify listeners of the change of the zoom area. 37 */ 38 public interface ZoomChangeListener { 39 /** 40 * Method called when the zoom area has changed. 41 * @param navigationModel The model firing the change. 42 * @param oldZoom The old zoom. Might be null on initial zoom. 43 * @param newZoom The new zoom. 44 */ 45 void zoomChanged(NavigationModel navigationModel, ZoomData oldZoom, ZoomData newZoom); 46 } 47 48 /** 49 * This is a weak reference to a zoom listener. The weak reference auto-removes itsef if the referenced zoom change listener is no longer used. 50 * @author michael 51 * 52 */ 53 public static class WeakZoomChangeListener implements ZoomChangeListener { 54 private WeakReference<ZoomChangeListener> l; 55 56 /** 57 * Creates a new, weak zoom listener. 58 * @param l The listener. 59 */ 60 public WeakZoomChangeListener(ZoomChangeListener l) { 61 // Note: We might use reference queues to clear the reference earlier. 62 this.l = new WeakReference<>(l); 63 } 64 65 @Override 66 public void zoomChanged(NavigationModel navigationModel, ZoomData oldZoom, ZoomData newZoom) { 67 ZoomChangeListener listener = l.get(); 68 if (listener != null) { 69 listener.zoomChanged(navigationModel, oldZoom, newZoom); 70 } else { 71 navigationModel.removeZoomChangeListener(listener); 72 } 73 } 74 } 75 76 /** 77 * The mode that is used to zoom to a given position on the map. Modes influence how the zoom undo stack is handled and if smooth zooming is used. 78 * @author Michael Zangl 79 */ 80 public enum ScrollMode { 81 /** 82 * An initial zoom. This resets the zoom history and zooms immediately. 83 */ 84 INITIAL, 85 /** 86 * Use the default scroll mode. 87 */ 88 DEFAULT, 89 /** 90 * Immeadiately zoom to the position. 91 */ 92 IMMEDIATE, 93 /** 94 * Animate a smooth, slow move to the position. 95 */ 96 ANIMATE, 97 /** 98 * Animate a relatively fast change (200ms). 99 */ 100 ANIMATE_FAST; 101 102 // Replace this with better methods? 103 private boolean resetHistory() { 104 return this == INITIAL; 105 } 106 107 private int animationTime() { 108 if (this == ANIMATE) { 109 return 1500; 110 } else if (this == ANIMATE_FAST) { 111 return 200; 112 } else { 113 return 0; 114 } 115 } 116 } 117 118 /** 119 * This stores a position on the screen (relative to one projection). 120 * @author michael 121 * 122 */ 123 public static class ZoomData { 124 /** 125 * Center n/e coordinate of the desired screen center using the projection when this object was created. 126 */ 127 private final EastNorth center; 128 129 /** 130 * The scale factor in x or y-units per pixel. This means, if scale = 10, 131 * every physical pixel on screen are 10 x or 10 y units in the 132 * northing/easting space of the projection. 133 */ 134 private final double scale; 135 136 /** 137 * The projection used to compute this center. 138 */ 139 private final Projection usedProjection; 140 141 /** 142 * Create a new {@link ZoomData} with any content. 143 */ 144 public ZoomData() { 145 this(new EastNorth(0, 0), 1); 146 } 147 148 /** 149 * Interpolates between two zoom data instances. 150 * @param otherZoom The other zoom 151 * @param proportion How much the other zoom object influences the result so that the values (0..1) form a straight line between the two centers. 152 * @param projection The projection to used. Currently, we interpolate in EastNorth coordinates, but this could change (180° problem, ...). 153 * @return A new, interpolated ZoomData. 154 */ 155 public ZoomData interpolate(ZoomData otherZoom, double proportion, Projection projection) { 156 EastNorth from = getCenterEastNorth(projection); 157 EastNorth to = otherZoom.getCenterEastNorth(projection); 158 EastNorth currentCenter = from.interpolate(to, proportion); 159 double currentScale = (1 - proportion) * getScale() + proportion * otherZoom.getScale(); 160 return new ZoomData(currentCenter, currentScale, projection); 161 } 162 163 /** 164 * Create a new {@link ZoomData} using no specified projection. 165 * @param center The center to store. 166 * @param scale The scale to store. 167 */ 168 public ZoomData(EastNorth center, double scale) { 169 this(center, scale, null); 170 } 171 172 /** 173 * Create a new {@link ZoomData} specified using the given projection. 174 * @param center The center to store. 175 * @param scale The scale to store. 176 * @param usedProjection The projection in which the center is. 177 */ 178 public ZoomData(EastNorth center, double scale, Projection usedProjection) { 179 this.center = center; 180 this.scale = scale; 181 this.usedProjection = usedProjection; 182 } 183 184 /** 185 * Gets the center position. 186 * @param projection The projection to use to get the center. If this is not the projection this object was constructed with, the EastNorth position of the center in the new projection is returned. 187 * @return The center. 188 */ 189 public EastNorth getCenterEastNorth(Projection projection) { 190 if (usedProjection == null || projection == null || usedProjection == projection) { 191 return center; 192 } else { 193 // we need to project the coordinates using the new projection. 194 LatLon latlon = usedProjection.eastNorth2latlon(center); 195 return projection.latlon2eastNorth(latlon); 196 } 197 } 198 199 /** 200 * Gets the scale. 201 * @return The scale. 202 */ 203 public double getScale() { 204 return scale; 205 } 206 207 /** 208 * Checks if this ZoomData instance is almost the same as an other instance. 209 * @param otherData THe other instance. 210 * @return <code>true</code> if the centers are the same and the scale only differers a small amount. 211 */ 212 public boolean isWithinTolerance(ZoomData otherData) { 213 return otherData.center.equals(this.center) && Utils.equalsEpsilon(otherData.scale, scale) 214 && otherData.usedProjection == usedProjection; 215 } 216 217 /** 218 * Creates a new {@link ZoomData} that uses the new projection as base. This improves performance but has no other impacts on the behavior of the object. 219 * @param projection The projection 220 * @return A new, optimized {@link ZoomData} 221 */ 222 public ZoomData usingProjection(Projection projection) { 223 return new ZoomData(getCenterEastNorth(projection), getScale(), projection); 224 } 225 226 @Override 227 public int hashCode() { 228 final int prime = 31; 229 int result = 1; 230 result = prime * result + ((center == null) ? 0 : center.hashCode()); 231 long temp; 232 temp = Double.doubleToLongBits(scale); 233 result = prime * result + (int) (temp ^ (temp >>> 32)); 234 result = prime * result + ((usedProjection == null) ? 0 : usedProjection.hashCode()); 235 return result; 236 } 237 238 @Override 239 public boolean equals(Object obj) { 240 if (this == obj) 241 return true; 242 if (obj == null) 243 return false; 244 if (getClass() != obj.getClass()) 245 return false; 246 ZoomData other = (ZoomData) obj; 247 if (center == null) { 248 if (other.center != null) 249 return false; 250 } else if (!center.equals(other.center)) 251 return false; 252 if (Double.doubleToLongBits(scale) != Double.doubleToLongBits(other.scale)) 253 return false; 254 if (usedProjection == null) { 255 if (other.usedProjection != null) 256 return false; 257 } else if (!usedProjection.equals(other.usedProjection)) 258 return false; 259 return true; 260 } 261 262 @Override 263 public String toString() { 264 return "ZoomData [center=" + center + ", scale=" + scale + ", usedProjection=" + usedProjection + "]"; 265 } 266 267 /** 268 * Gets the affine transform that converts the east/north coordinates to pixel coordinates with no view offset. The center of the view would be at (0,0) 269 * @return The current affine transform. 270 */ 271 public AffineTransform getAffineTransform() { 272 return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -center.east() / scale, center.north() 273 / scale); 274 } 275 276 } 277 278 private static class ZoomHistoryStack extends Stack<ZoomData> { 279 @Override 280 public ZoomData push(ZoomData item) { 281 ZoomData pushResult = super.push(item); 282 if (size() > Main.pref.getInteger("zoom.undo.max", 50)) { 283 remove(0); 284 } 285 return pushResult; 286 } 287 } 288 289 /** 290 * A {@link TimerTask} that is used for zoom to animations. 291 * @author Michael Zangl 292 * 293 */ 294 private final class AnimateZoomToTimerTask extends TimerTask { 295 private final int animationTime; 296 private int time = 0; 297 private final ZoomData currentZoom; 298 private final ZoomData newZoom; 299 300 private AnimateZoomToTimerTask(int animationTime, ZoomData currentZoom, ZoomData newZoom) { 301 this.animationTime = animationTime; 302 this.currentZoom = currentZoom; 303 this.newZoom = newZoom; 304 } 305 306 @Override 307 public void run() { 308 double progress = Math.min((double) time / animationTime, 1); 309 310 // Make animation smooth 311 progress = (1 - Math.cos(progress * Math.PI)) / 2; 312 final ZoomData position = currentZoom.interpolate(newZoom, progress, getProjection()); 313 314 GuiHelper.runInEDT(new Runnable() { 315 @Override 316 public void run() { 317 realZoomToNoUndo(position, true); 318 } 319 }); 320 321 if (time >= animationTime) { 322 cancel(); 323 } else { 324 time += TIMER_PERIOD; 325 } 326 } 327 } 328 329 // 20 FPS should be enough. 330 private static final long TIMER_PERIOD = 50; 331 332 /** 333 * The current center/scale that is used. 334 */ 335 private ZoomData currentZoom = new ZoomData(); 336 337 /** 338 * The size of the navigation view. It is used to translate pixel coordinates. 339 */ 340 private Dimension viewDimension = new Dimension(0, 0); 341 342 private final ZoomHistoryStack zoomUndoBuffer = new ZoomHistoryStack(); 343 private final ZoomHistoryStack zoomRedoBuffer = new ZoomHistoryStack(); 344 private Date zoomTimestamp = new Date(); 345 346 /** 347 * the zoom listeners 348 */ 349 private final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>(); 350 351 /** 352 * A weak reference to the component of which we are tracking the size. That way, that component can get garbage collected while this model is still active. 353 */ 354 private WeakReference<Component> trackedComponent = new WeakReference<Component>(null); 355 356 private final ComponentAdapter resizeAdapter = new ComponentAdapter() { 357 @Override 358 public void componentResized(ComponentEvent e) { 359 setViewportSize(e.getComponent().getSize()); 360 } 361 @Override 362 public void componentShown(ComponentEvent e) { 363 componentResized(e); 364 } 365 }; 366 367 private Timer zoomToTimer; 368 369 /** 370 * The zoomTo animation that is currently running. 371 */ 372 private TimerTask currentZoomToAnimation; 373 374 /** 375 * @return Returns the center point. A copy is returned, so users cannot 376 * change the center by accessing the return value. Use zoomTo instead. 377 */ 378 public EastNorth getCenter() { 379 return currentZoom.getCenterEastNorth(getProjection()); 380 } 381 382 /** 383 * Get the current scale factor. This is [delta in eastnorth]/[pixels]. 384 * @return The scale. 385 */ 386 public double getScale() { 387 return currentZoom.getScale(); 388 } 389 390 /** 391 * Starts to listen to size change events for that component and adjusts our reference size whenever that component size changed. 392 * @param component The component to track. 393 */ 394 public void trackComponentSize(Component component) { 395 Component trackedComponent = this.trackedComponent.get(); 396 if (trackedComponent != null) { 397 trackedComponent.removeComponentListener(resizeAdapter); 398 } 399 component.addComponentListener(resizeAdapter); 400 this.trackedComponent = new WeakReference<Component>(component); 401 setViewportSize(component.getSize()); 402 } 403 404 protected void setViewportSize(Dimension size) { 405 if (!size.equals(viewDimension)) { 406 this.viewDimension = size; 407 fireZoomChanged(currentZoom, currentZoom); 408 } 409 } 410 411 /** 412 * Zoom to the given coordinate while preserving the current scale. 413 * 414 * @param newCenter The center to zoom to. 415 * @param mode The animation mode to use for zooming. 416 */ 417 public void zoomTo(EastNorth newCenter, ScrollMode mode) { 418 zoomTo(newCenter, getScale(), mode); 419 } 420 421 /** 422 * Zoom to the given coordinate and scale. 423 * 424 * @param newCenter The center to zoom to. 425 * @param newScale The scale to use. 426 */ 427 public void zoomTo(EastNorth newCenter, double newScale) { 428 zoomTo(newCenter, newScale, ScrollMode.DEFAULT); 429 } 430 431 /** 432 * Zoom to the given coordinate and scale. 433 * 434 * @param newCenter The center to zoom to. 435 * @param newScale The scale to use. 436 * @param mode The animation mode to use for zooming. 437 */ 438 public void zoomTo(EastNorth newCenter, double newScale, ScrollMode mode) { 439 if (newScale <= 0) { 440 throw new IllegalArgumentException("Scale (" + newScale + ") may not be negative."); 441 } 442 Bounds b = getProjection().getWorldBoundsLatLon(); 443 LatLon cl = Projections.inverseProject(newCenter); 444 boolean changed = false; 445 double lat = cl.lat(); 446 double lon = cl.lon(); 447 if (lat < b.getMinLat()) { 448 changed = true; 449 lat = b.getMinLat(); 450 } else if (lat > b.getMaxLat()) { 451 changed = true; 452 lat = b.getMaxLat(); 453 } 454 if (lon < b.getMinLon()) { 455 changed = true; 456 lon = b.getMinLon(); 457 } else if (lon > b.getMaxLon()) { 458 changed = true; 459 lon = b.getMaxLon(); 460 } 461 if (changed) { 462 newCenter = Projections.project(new LatLon(lat, lon)); 463 } 464 int centerX = viewDimension.width / 2; 465 int centerY = viewDimension.height / 2; 466 LatLon l1 = new LatLon(b.getMinLat(), lon); 467 LatLon l2 = new LatLon(b.getMaxLat(), lon); 468 EastNorth e1 = getProjection().latlon2eastNorth(l1); 469 EastNorth e2 = getProjection().latlon2eastNorth(l2); 470 double d = e2.north() - e1.north(); 471 if (centerY > 0 && d < centerY * newScale) { 472 double newScaleH = d / centerY; 473 e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMinLon())); 474 e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMaxLon())); 475 d = e2.east() - e1.east(); 476 if (centerX > 0 && d < centerX * newScale) { 477 newScale = Math.max(newScaleH, d / centerX); 478 } 479 } else if (centerY > 0) { 480 d = d / (l1.greatCircleDistance(l2) * centerY * 10); 481 if (newScale < d) { 482 newScale = d; 483 } 484 } 485 486 ZoomData newZoom = new ZoomData(newCenter, newScale, getProjection()); 487 if (!newZoom.isWithinTolerance(currentZoom)) { 488 realZoomTo(newZoom, mode); 489 } 490 } 491 492 /** 493 * Zooms around a given point on the screen by a given factor. 494 * <p> 495 * The EastNorth position below that point on the screen will stay the same. 496 * @param screenPosition The position on the screen to zoom around. 497 * @param factor The factor to zoom by. 498 */ 499 public void zoomToFactorAround(Point2D screenPosition, double factor) { 500 double newScale = getScale() * factor; 501 // New center position so that point under the mouse pointer stays the same place as it was before zooming 502 // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom 503 zoomTo(new EastNorth(getCenter().east() - (screenPosition.getX() - viewDimension.width / 2.0) 504 * (newScale - getScale()), getCenter().north() + (screenPosition.getY() - viewDimension.height / 2.0) 505 * (newScale - getScale())), newScale); 506 } 507 508 /** 509 * Zoom to a position without checking it. 510 * @param newZoom The new zoom. 511 * @param mode The zoom mode 512 */ 513 private void realZoomTo(ZoomData newZoom, ScrollMode mode) { 514 if (mode.resetHistory()) { 515 zoomRedoBuffer.clear(); 516 zoomUndoBuffer.clear(); 517 } else { 518 pushZoomUndo(newZoom); 519 } 520 realZoomToNoUndo(newZoom, mode); 521 } 522 523 private void realZoomToNoUndo(ZoomData newZoom, ScrollMode mode) { 524 final int animationTime = mode.animationTime(); 525 if (animationTime > 0) { 526 if (currentZoomToAnimation != null) { 527 currentZoomToAnimation.cancel(); 528 } 529 currentZoomToAnimation = new AnimateZoomToTimerTask(animationTime, currentZoom, newZoom); 530 if (zoomToTimer == null) { 531 zoomToTimer = new Timer("Zoom animation."); 532 } 533 zoomToTimer.schedule(currentZoomToAnimation, 0, TIMER_PERIOD); 534 } else { 535 realZoomToNoUndo(newZoom, mode != ScrollMode.INITIAL); 536 } 537 } 538 539 private void realZoomToNoUndo(ZoomData newZoom, boolean passOldZoomToListeners) { 540 if (!newZoom.equals(currentZoom)) { 541 ZoomData oldZoom = currentZoom; 542 currentZoom = newZoom; 543 // XXX: Do not fire if mode is initial ? 544 fireZoomChanged(passOldZoomToListeners ? oldZoom : null, currentZoom); 545 } 546 } 547 548 // ================ Zoom undo and redo ================ 549 550 private void pushZoomUndo(ZoomData zoomData) { 551 Date now = new Date(); 552 if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) { 553 zoomUndoBuffer.push(zoomData); 554 zoomRedoBuffer.clear(); 555 } 556 zoomTimestamp = now; 557 } 558 559 /** 560 * Zoom to the previous zoom position. This call is ignored if there is no previous position. 561 */ 562 public void zoomPrevious() { 563 zoomInBuffer(zoomUndoBuffer, zoomRedoBuffer); 564 } 565 566 /** 567 * Zoom to the next zoom position. This call is ignored if there is no next position. 568 */ 569 public void zoomNext() { 570 zoomInBuffer(zoomRedoBuffer, zoomUndoBuffer); 571 } 572 573 private void zoomInBuffer(ZoomHistoryStack takeFrom, ZoomHistoryStack pushTo) { 574 if (!takeFrom.isEmpty()) { 575 ZoomData zoom = takeFrom.pop(); 576 pushTo.push(currentZoom); 577 realZoomToNoUndo(zoom.usingProjection(getProjection()), ScrollMode.DEFAULT); 578 } 579 } 580 581 /** 582 * Check if there are previous zoom entries. 583 * @return <code>true</code> if there are previous zoom entries and {@link #zoomPrevious()} can be used to zoom to them. 584 */ 585 public boolean hasPreviousZoomEntries() { 586 return !zoomUndoBuffer.isEmpty(); 587 } 588 589 /** 590 * Check if there are next zoom entries. 591 * @return <code>true</code> if there are next zoom entries and {@link #zoomNext()} can be used to zoom to them. 592 */ 593 public boolean hasNextZoomEntries() { 594 return !zoomRedoBuffer.isEmpty(); 595 } 596 597 598 599 // ================ Screen/EastNorth/LatLon conversion ================ 600 601 /** 602 * Get the NorthEast coordinate for a given screen position. 603 * @param x X-Pixelposition to get coordinate from 604 * @param y Y-Pixelposition to get coordinate from 605 * 606 * @return Geographic coordinates from a specific pixel coordination on the screen. 607 */ 608 public EastNorth getEastNorth(double x, double y) { 609 return new EastNorth(getCenter().east() + (x - viewDimension.width / 2.0) * getScale(), getCenter().north() 610 - (y - viewDimension.height / 2.0) * getScale()); 611 } 612 613 /** 614 * Get the NorthEast coordinate for a given screen position. 615 * @param point The screen position 616 * 617 * @return Geographic coordinates from a specific pixel coordination on the screen. 618 */ 619 public EastNorth getEastNorth(Point2D point) { 620 return getEastNorth(point.getX(), point.getY()); 621 } 622 623 /** 624 * Gets an EastNorth position using relative screen coordinates. 625 * @param relativeX The x-positon, where the interval [0,1] is the screen width 626 * @param relativeY The x-positon, where the interval [0,1] is the screen height 627 * @return The geographic coordinates for that pixel. 628 */ 629 public EastNorth getEastNorthRelative(double relativeX, double relativeY) { 630 return getEastNorth(relativeX * viewDimension.width, relativeY * viewDimension.height); 631 } 632 633 /** 634 * Get the lat/lon coordinate for a given screen position. 635 * @param point The screen position 636 * 637 * @return Geographic coordinates from a specific pixel coordination on the screen. 638 */ 639 public LatLon getLatLon(Point2D point) { 640 return getProjection().eastNorth2latlon(getEastNorth(point)); 641 } 642 643 /** 644 * Converts an east/north coordinate to a screen position. 645 * @param eastNorth The point to convert. 646 * @return An arbitrary point if p is <code>null</code>, the screen position (may be outside the screen) otherwise. 647 */ 648 public Point2D getScreenPosition(EastNorth eastNorth) { 649 if (null == eastNorth) { 650 return new Point(); 651 } else { 652 Point2D p2d = new Point2D.Double(eastNorth.east(), eastNorth.north()); 653 return getAffineTransform().transform(p2d, null); 654 } 655 } 656 657 /** 658 * Converts a latlon coordinate to a screen position. 659 * @param latlon The point to convert. 660 * @return An arbitrary point if p is <code>null</code>, the screen position (may be outside the screen) otherwise. 661 */ 662 public Point2D getScreenPosition(LatLon latlon) { 663 if (latlon == null) { 664 return new Point(); 665 } else if (latlon instanceof CachedLatLon) { 666 return getScreenPosition(((CachedLatLon)latlon).getEastNorth()); 667 } else { 668 return getScreenPosition(getProjection().latlon2eastNorth(latlon)); 669 } 670 } 671 672 /** 673 * Gets the affine transform that converts the east/north coordinates to pixel coordinates. 674 * @return The current affine transform. Do not modify it. 675 */ 676 public AffineTransform getAffineTransform() { 677 AffineTransform transform = AffineTransform.getTranslateInstance(viewDimension.width / 2, 678 viewDimension.height / 2); 679 transform.concatenate(currentZoom.getAffineTransform()); 680 return transform; 681 } 682 683 /** 684 * Gets the horizontal distance in meters that a line of the length of n pixels would cover in the center of our view. 685 * @param pixel The number of pixels the line should have. 686 * @return The length in meters. 687 */ 688 public double getPixelDistance(int pixel) { 689 double centerX = viewDimension.getWidth() / 2; 690 double centerY = viewDimension.getHeight() / 2; 691 LatLon ll1 = getLatLon(new Point2D.Double(centerX - pixel / 2.0, centerY)); 692 LatLon ll2 = getLatLon(new Point2D.Double(centerX + pixel / 2.0, centerY)); 693 return ll1.greatCircleDistance(ll2); 694 } 695 696 // ================ Zoom change listeners ================ 697 698 /** 699 * @return The projection to be used in calculating stuff. 700 */ 701 private Projection getProjection() { 702 return Main.getProjection(); 703 } 704 705 /** 706 * Adds a zoom change listener 707 * 708 * @param listener the listener. Ignored if null or already registered. 709 */ 710 public void addZoomChangeListener(ZoomChangeListener listener) { 711 if (listener != null) { 712 zoomChangeListeners.addIfAbsent(listener); 713 } 714 } 715 716 /** 717 * Removes a zoom change listener 718 * 719 * @param listener the listener. Ignored if null or already absent 720 */ 721 public void removeZoomChangeListener(ZoomChangeListener listener) { 722 zoomChangeListeners.remove(listener); 723 } 724 725 protected void fireZoomChanged(ZoomData oldZoom, ZoomData currentZoom) { 726 for (ZoomChangeListener l : zoomChangeListeners) { 727 l.zoomChanged(this, oldZoom, currentZoom); 728 } 729 } 730 } -
test/unit/org/openstreetmap/josm/TestUtils.java
diff --git a/test/unit/org/openstreetmap/josm/TestUtils.java b/test/unit/org/openstreetmap/josm/TestUtils.java index 729e511..678fce0 100644
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm; 3 3 4 import static org.junit.Assert.assertEquals; 4 5 import static org.junit.Assert.fail; 5 6 7 import java.awt.geom.Point2D; 6 8 import java.util.Arrays; 7 9 import java.util.Comparator; 8 10 11 import org.openstreetmap.josm.data.coor.EastNorth; 12 import org.openstreetmap.josm.data.coor.LatLon; 13 9 14 /** 10 15 * Various utils, useful for unit tests. 11 16 */ … … public final class TestUtils { 106 111 .append("\nCompared\no2: ").append(o2).append("\no3: ").append(o3).append("\ngave: ").append(d) 107 112 .toString(); 108 113 } 114 115 /** 116 * An assertion that fails if the provided coordinates are not the same (within the default server precision). 117 * @param expected The expected EastNorth coordinate. 118 * @param actual The actual value. 119 */ 120 public static void assertEastNorthEquals(EastNorth expected, EastNorth actual) { 121 assertEquals("Wrong x coordinate.", expected.getX(), actual.getX(), LatLon.MAX_SERVER_PRECISION); 122 assertEquals("Wrong y coordinate.", expected.getY(), actual.getY(), LatLon.MAX_SERVER_PRECISION); 123 } 124 125 /** 126 * An assertion that fails if the provided coordinates are not the same (within the default server precision). 127 * @param expected The expected LatLon coordinate. 128 * @param actual The actual value. 129 */ 130 public static void assertLatLonEquals(LatLon expected, LatLon actual) { 131 assertEquals("Wrong lat coordinate.", expected.getX(), actual.getX(), LatLon.MAX_SERVER_PRECISION); 132 assertEquals("Wrong lon coordinate.", expected.getY(), actual.getY(), LatLon.MAX_SERVER_PRECISION); 133 } 134 135 /** 136 * An assertion that fails if the provided points are not the same. 137 * @param expected The expected Point2D 138 * @param actual The actual value. 139 */ 140 public static void assertPointEquals(Point2D expected, Point2D actual) { 141 if (expected.distance(actual) > 0.0000001) { 142 throw new AssertionError("Expected " + expected + " but got " + actual); 143 } 144 } 109 145 } -
new file test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java
diff --git a/test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java b/test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java new file mode 100644 index 0000000..2e12d0f
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui; 3 4 import static org.junit.Assert.assertEquals; 5 import static org.openstreetmap.josm.TestUtils.assertEastNorthEquals; 6 import static org.openstreetmap.josm.TestUtils.assertLatLonEquals; 7 import static org.openstreetmap.josm.TestUtils.assertPointEquals; 8 9 import java.awt.Rectangle; 10 import java.awt.geom.Point2D; 11 12 import org.junit.Before; 13 import org.junit.BeforeClass; 14 import org.junit.Test; 15 import org.openstreetmap.josm.JOSMFixture; 16 import org.openstreetmap.josm.Main; 17 import org.openstreetmap.josm.data.Bounds; 18 import org.openstreetmap.josm.data.ProjectionBounds; 19 import org.openstreetmap.josm.data.coor.EastNorth; 20 import org.openstreetmap.josm.data.coor.LatLon; 21 import org.openstreetmap.josm.gui.util.GuiHelper; 22 23 /** 24 * Some tests for the {@link NavigatableComponent} class. 25 * @author Michael Zangl 26 * 27 */ 28 public class NavigatableComponentTest { 29 30 private static final int HEIGHT = 200; 31 private static final int WIDTH = 300; 32 private NavigatableComponent component; 33 34 /** 35 * Setup test. 36 */ 37 @BeforeClass 38 public static void setUp() { 39 JOSMFixture.createUnitTestFixture().init(); 40 } 41 42 /** 43 * Create a new, fresh {@link NavigatableComponent} 44 */ 45 @Before 46 public void setup() { 47 component = new NavigatableComponent(); 48 component.setBounds(new Rectangle(WIDTH, HEIGHT)); 49 // wait for the event to be propagated. 50 GuiHelper.runInEDTAndWait(new Runnable() { 51 @Override 52 public void run() { 53 } 54 }); 55 } 56 57 /** 58 * Test if the default scale was set correctly. 59 */ 60 @Test 61 public void testDefaultScale() { 62 assertEquals(Main.getProjection().getDefaultZoomInPPD(), component.getScale(), 0.00001); 63 } 64 65 /** 66 * Tests {@link NavigatableComponent#getPoint2D(EastNorth)} 67 */ 68 @Test 69 public void testPoint2DEastNorth() { 70 assertPointEquals(new Point2D.Double(), component.getPoint2D((EastNorth) null)); 71 Point2D shouldBeCenter = component.getPoint2D(component.getCenter()); 72 assertPointEquals(new Point2D.Double(WIDTH / 2, HEIGHT / 2), shouldBeCenter); 73 74 EastNorth testPoint = component.getCenter().add(300 * component.getScale(), 200 * component.getScale()); 75 Point2D testPointConverted = component.getPoint2D(testPoint); 76 assertPointEquals(new Point2D.Double(WIDTH / 2 + 300, HEIGHT / 2 - 200), testPointConverted); 77 } 78 79 /** 80 * TODO: Implement this test. 81 */ 82 @Test 83 public void testPoint2DLatLon() { 84 assertPointEquals(new Point2D.Double(), component.getPoint2D((LatLon) null)); 85 // TODO: Really test this. 86 } 87 88 /** 89 * Tests {@link NavigatableComponent#zoomTo(LatLon) 90 */ 91 @Test 92 public void testZoomToLatLon() { 93 component.zoomTo(new LatLon(10, 10)); 94 Point2D shouldBeCenter = component.getPoint2D(new LatLon(10, 10)); 95 assertPointEquals(new Point2D.Double(WIDTH / 2, HEIGHT / 2), shouldBeCenter); 96 } 97 98 /** 99 * Tests {@link NavigatableComponent#zoomToFactor(double)} and {@link NavigatableComponent#zoomToFactor(EastNorth, double)} 100 */ 101 @Test 102 public void testZoomToFactor() { 103 EastNorth center = component.getCenter(); 104 double initialScale = component.getScale(); 105 106 // zoomToFactor(double) 107 component.zoomToFactor(0.5); 108 assertEquals(initialScale / 2, component.getScale(), 0.00000001); 109 assertEastNorthEquals(center, component.getCenter()); 110 component.zoomToFactor(2); 111 assertEquals(initialScale, component.getScale(), 0.00000001); 112 assertEastNorthEquals(center, component.getCenter()); 113 114 // zoomToFactor(EastNorth, double) 115 EastNorth newCenter = new EastNorth(10, 20); 116 component.zoomToFactor(newCenter, 0.5); 117 assertEquals(initialScale / 2, component.getScale(), 0.00000001); 118 assertEastNorthEquals(newCenter, component.getCenter()); 119 component.zoomToFactor(newCenter, 2); 120 assertEquals(initialScale, component.getScale(), 0.00000001); 121 assertEastNorthEquals(newCenter, component.getCenter()); 122 } 123 124 /** 125 * Tests {@link NavigatableComponent#getEastNorth(int, int) 126 */ 127 @Test 128 public void testGetEastNorth() { 129 EastNorth center = component.getCenter(); 130 assertEastNorthEquals(center, component.getEastNorth(WIDTH / 2, HEIGHT / 2)); 131 132 EastNorth testPoint = component.getCenter().add(WIDTH * component.getScale(), HEIGHT * component.getScale()); 133 assertEastNorthEquals(testPoint, component.getEastNorth(3 * WIDTH / 2, -HEIGHT / 2)); 134 } 135 136 /** 137 * Tests {@link NavigatableComponent#zoomToFactor(double, double, double) 138 */ 139 @Test 140 public void testZoomToFactorCenter() { 141 // zoomToFactor(double, double, double) 142 // assumes getEastNorth works as expected 143 EastNorth testPoint1 = component.getEastNorth(0, 0); 144 EastNorth testPoint2 = component.getEastNorth(200, 150); 145 double initialScale = component.getScale(); 146 147 component.zoomToFactor(0, 0, 0.5); 148 assertEquals(initialScale / 2, component.getScale(), 0.00000001); 149 assertEastNorthEquals(testPoint1, component.getEastNorth(0, 0)); 150 component.zoomToFactor(0, 0, 2); 151 assertEquals(initialScale, component.getScale(), 0.00000001); 152 assertEastNorthEquals(testPoint1, component.getEastNorth(0, 0)); 153 154 component.zoomToFactor(200, 150, 0.5); 155 assertEquals(initialScale / 2, component.getScale(), 0.00000001); 156 assertEastNorthEquals(testPoint2, component.getEastNorth(200, 150)); 157 component.zoomToFactor(200, 150, 2); 158 assertEquals(initialScale, component.getScale(), 0.00000001); 159 assertEastNorthEquals(testPoint2, component.getEastNorth(200, 150)); 160 161 } 162 163 /** 164 * Tests {@link NavigatableComponent#getProjectionBounds()} 165 */ 166 @Test 167 public void testGetProjectionBounds() { 168 ProjectionBounds bounds = component.getProjectionBounds(); 169 assertEastNorthEquals(component.getCenter(), bounds.getCenter()); 170 171 assertEastNorthEquals(component.getEastNorth(0, HEIGHT), bounds.getMin()); 172 assertEastNorthEquals(component.getEastNorth(WIDTH, 0), bounds.getMax()); 173 } 174 175 /** 176 * Tests {@link NavigatableComponent#getRealBounds()} 177 */ 178 @Test 179 public void testGetRealBounds() { 180 Bounds bounds = component.getRealBounds(); 181 assertLatLonEquals(component.getLatLon(WIDTH / 2, HEIGHT / 2), bounds.getCenter()); 182 183 assertLatLonEquals(component.getLatLon(0, HEIGHT), bounds.getMin()); 184 assertLatLonEquals(component.getLatLon(WIDTH, 0), bounds.getMax()); 185 } 186 187 }
