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 michael2402, 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;  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
     6import java.awt.Component;
    67import java.awt.Cursor;
    78import java.awt.Point;
    89import java.awt.event.ActionEvent;
    import java.awt.event.MouseEvent;  
    1213import java.awt.event.MouseMotionListener;
    1314import java.awt.event.MouseWheelEvent;
    1415import java.awt.event.MouseWheelListener;
     16import java.awt.geom.Point2D;
    1517
    1618import javax.swing.AbstractAction;
    1719import javax.swing.ActionMap;
    import javax.swing.KeyStroke;  
    2325import org.openstreetmap.josm.Main;
    2426import org.openstreetmap.josm.actions.mapmode.SelectAction;
    2527import org.openstreetmap.josm.data.coor.EastNorth;
     28import org.openstreetmap.josm.gui.navigate.NavigationCursorManager;
     29import org.openstreetmap.josm.gui.navigate.NavigationModel;
     30import org.openstreetmap.josm.gui.navigate.NavigationModel.ScrollMode;
    2631import org.openstreetmap.josm.tools.Destroyable;
    2732import org.openstreetmap.josm.tools.Shortcut;
    2833
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    4449        @Override
    4550        public void actionPerformed(ActionEvent e) {
    4651            if (".".equals(action) || ",".equals(action)) {
    47                 Point mouse = nc.getMousePosition();
     52                Point2D mouse = lastMousePosition;
    4853                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
    5157                        MouseWheelEvent.WHEEL_UNIT_SCROLL, 1, ",".equals(action) ? -1 : 1);
    5258                mouseWheelMoved(we);
    5359            } 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;
    5662                switch(action) {
    5763                case "left":
    58                     nc.zoomTo(new EastNorth(2*center.east()-newcenter.east(), center.north()));
     64                    relativeX -= .2;
    5965                    break;
    6066                case "right":
    61                     nc.zoomTo(new EastNorth(newcenter.east(), center.north()));
     67                    relativeX += .2;
    6268                    break;
    6369                case "up":
    64                     nc.zoomTo(new EastNorth(center.east(), 2*center.north()-newcenter.north()));
     70                    relativeY -= .2;
    6571                    break;
    6672                case "down":
    67                     nc.zoomTo(new EastNorth(center.east(), newcenter.north()));
     73                    relativeY += .2;
    6874                    break;
    6975                }
     76                EastNorth newcenter = nm.getEastNorthRelative(relativeX, relativeY);
     77                nm.zoomTo(newcenter, ScrollMode.IMMEDIATE);
    7078            }
    7179        }
    7280    }
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    7987    /**
    8088     * The map to move around.
    8189     */
    82     private final NavigatableComponent nc;
     90    private final NavigationModel nm;
    8391    private final JPanel contentPane;
    8492
    8593    private boolean movementInPlace = false;
     94    private final NavigationCursorManager cursorManager;
     95
     96    private Point lastMousePosition = null;
    8697
    8798    /**
    8899     * 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.
    90102     * @param contentPane the content pane
    91103     */
    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;
    94107        this.contentPane = contentPane;
    95         nc.addMouseListener(this);
    96         nc.addMouseMotionListener(this);
    97         nc.addMouseWheelListener(this);
    98108
    99109        if (contentPane != null) {
    100110            // CHECKSTYLE.OFF: LineLength
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    137147    }
    138148
    139149    /**
     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    /**
    140160     * If the right (and only the right) mouse button is pressed, move the map.
    141161     */
    142162    @Override
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    150170        if (stdMovement || (macMovement && allowedMode)) {
    151171            if (mousePosMove == null)
    152172                startMovement(e);
    153             EastNorth center = nc.getCenter();
    154             EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY());
    155             nc.zoomTo(new EastNorth(
     173            EastNorth center = nm.getCenter();
     174            EastNorth mouseCenter = nm.getEastNorth(e.getPoint());
     175            nm.zoomTo(new EastNorth(
    156176                    mousePosMove.east() + center.east() - mouseCenter.east(),
    157                     mousePosMove.north() + center.north() - mouseCenter.north()));
     177                    mousePosMove.north() + center.north() - mouseCenter.north()), ScrollMode.IMMEDIATE);
    158178        } else {
    159179            endMovement();
    160180        }
     181        updateMousePosition(e);
    161182    }
    162183
    163184    /**
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    171192                Main.isPlatformOsx() && e.getModifiersEx() == macMouseMask) {
    172193            startMovement(e);
    173194        }
     195        updateMousePosition(e);
    174196    }
    175197
    176198    /**
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    181203        if (e.getButton() == MouseEvent.BUTTON3 || Main.isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1) {
    182204            endMovement();
    183205        }
     206        updateMousePosition(e);
     207    }
     208
     209    @Override
     210    public void mouseExited(MouseEvent e) {
     211        lastMousePosition = null;
    184212    }
    185213
    186214    /**
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    192220        if (movementInPlace)
    193221            return;
    194222        movementInPlace = true;
    195         mousePosMove = nc.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);
    197225    }
    198226
    199227    /**
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    203231        if (!movementInPlace)
    204232            return;
    205233        movementInPlace = false;
    206         nc.resetCursor(this);
     234        cursorManager.resetCursor(this);
    207235        mousePosMove = null;
    208236    }
    209237
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    213241     */
    214242    @Override
    215243    public void mouseWheelMoved(MouseWheelEvent e) {
    216         nc.zoomToFactor(e.getX(), e.getY(), Math.pow(Math.sqrt(2), e.getWheelRotation()));
     244        nm.zoomToFactorAround(e.getPoint(), Math.pow(Math.sqrt(2), e.getWheelRotation()));
    217245    }
    218246
    219247    /**
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    230258                if (mousePosMove == null) {
    231259                    startMovement(e);
    232260                }
    233                 EastNorth center = nc.getCenter();
    234                 EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY());
    235                 nc.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);
    237265            } else {
    238266                endMovement();
    239267            }
    240268        }
     269        updateMousePosition(e);
    241270    }
    242271
    243272    @Override
    public class MapMover extends MouseAdapter implements MouseMotionListener, Mouse  
    264293            }
    265294        }
    266295    }
     296
     297    private void updateMousePosition(MouseEvent e) {
     298        lastMousePosition = e.getPoint();
     299    }
    267300}
  • 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  
    376376                        return; // exit, if new parent.
    377377
    378378                    // Do nothing, if required data is missing
    379                     if (ms.mousePos == null || mv.center == null) {
     379                    if (ms.mousePos == null || mv.getCenter() == null) {
    380380                        continue;
    381381                    }
    382382
    public class MapStatus extends JPanel implements Helpful, Destroyable, Preferenc  
    822822
    823823            @Override
    824824            public void mouseMoved(MouseEvent e) {
    825                 if (mv.center == null)
     825                if (mv.getCenter() == null)
    826826                    return;
    827827                // Do not update the view if ctrl is pressed.
    828828                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;  
    2626import java.util.Collection;
    2727import java.util.Collections;
    2828import java.util.LinkedHashSet;
     29import java.util.LinkedList;
    2930import java.util.List;
    3031import java.util.ListIterator;
    3132import java.util.Set;
    import org.openstreetmap.josm.gui.layer.OsmDataLayer;  
    6162import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
    6263import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
    6364import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
     65import org.openstreetmap.josm.gui.navigate.NavigationModel;
     66import org.openstreetmap.josm.gui.navigate.NavigationModel.ZoomData;
    6467import org.openstreetmap.josm.gui.util.GuiHelper;
    6568import org.openstreetmap.josm.tools.AudioPlayer;
    6669import org.openstreetmap.josm.tools.BugReportExceptionHandler;
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    279282    // Layers that wasn't changed since last paint
    280283    private final transient List<Layer> nonChangedLayers = new ArrayList<>();
    281284    private transient Layer changedLayer;
    282     private int lastViewID;
    283285    private boolean paintPreferencesChanged = true;
    284286    private Rectangle lastClipBounds = new Rectangle();
    285287    private transient MapMover mapMover;
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    304306                    MapView.this.add(c);
    305307                }
    306308
    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();
    308313            }
    309314        });
    310315
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    331336            }
    332337        });
    333338
    334         if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0) != null) {
     339        if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null) {
    335340            setFocusTraversalKeysEnabled(false);
    336341        }
    337342    }
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    727732            canUseBuffer = !paintPreferencesChanged;
    728733            paintPreferencesChanged = false;
    729734        }
    730         canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount &&
    731         lastViewID == getViewID() && lastClipBounds.contains(g.getClipBounds());
     735        canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount && lastClipBounds.contains(g.getClipBounds());
    732736        if (canUseBuffer) {
    733737            for (int i = 0; i < nonChangedLayers.size(); i++) {
    734738                if (visibleLayers.get(i) != nonChangedLayers.get(i)) {
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    775779        for (int i = 0; i < nonChangedLayersCount; i++) {
    776780            nonChangedLayers.add(visibleLayers.get(i));
    777781        }
    778         lastViewID = getViewID();
    779782        lastClipBounds = g.getClipBounds();
    780783
    781784        tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    10191022        if (layer == activeLayer)
    10201023            return false;
    10211024
    1022         Layer old = activeLayer;
    10231025        activeLayer = layer;
    10241026        if (setEditLayer) {
    10251027            setEditLayer(layers);
    implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer  
    11751177        }
    11761178    }
    11771179
     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     */
    11781191    private transient SelectionChangedListener repaintSelectionChangedListener = new SelectionChangedListener() {
    11791192        @Override
    11801193        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;  
    5252import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
    5353import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
    5454import org.openstreetmap.josm.gui.navigate.NavigationCursorManager;
     55import org.openstreetmap.josm.gui.navigate.NavigationModel;
     56import org.openstreetmap.josm.gui.navigate.NavigationModel.ScrollMode;
     57import org.openstreetmap.josm.gui.navigate.NavigationModel.WeakZoomChangeListener;
     58import org.openstreetmap.josm.gui.navigate.NavigationModel.ZoomData;
    5559import org.openstreetmap.josm.tools.Predicate;
    5660import org.openstreetmap.josm.tools.Utils;
    5761
    import org.openstreetmap.josm.tools.Utils;  
    6266 * @author imi
    6367 * @since 41
    6468 */
    65 public class NavigatableComponent extends JComponent implements Helpful {
     69public class NavigatableComponent extends JComponent implements Helpful, NavigationModel.ZoomChangeListener {
    6670
    6771    /**
    6872     * Interface to notify listeners of the change of the zoom area.
    public class NavigatableComponent extends JComponent implements Helpful {  
    7478        void zoomChanged();
    7579    }
    7680
     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
    77120    /**
    78121     * Interface to notify listeners of the change of the system of measurement.
    79122     * @since 6056
    public class NavigatableComponent extends JComponent implements Helpful {  
    101144    public static final String PROPNAME_SCALE  = "scale";
    102145
    103146    /**
    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.
    105149     */
    106     private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<>();
     150    private static final NavigationModel defaultNavigationModel = new NavigationModel();
    107151
    108152    /**
    109153     * Removes a zoom change listener
    public class NavigatableComponent extends JComponent implements Helpful {  
    111155     * @param listener the listener. Ignored if null or already absent
    112156     */
    113157    public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
    114         zoomChangeListeners.remove(listener);
     158        defaultNavigationModel.removeZoomChangeListener(new ZoomChangeAdapter(listener));
    115159    }
    116160
    117161    /**
    public class NavigatableComponent extends JComponent implements Helpful {  
    120164     * @param listener the listener. Ignored if null or already registered.
    121165     */
    122166    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));
    132168    }
    133169
    134 
    135170    /**
    136171     * Removes a SoM change listener
    137172     *
    public class NavigatableComponent extends JComponent implements Helpful {  
    175210        SystemOfMeasurement.setSystemOfMeasurement(somKey);
    176211    }
    177212
    178     private double scale = Main.getProjection().getDefaultZoomInPPD();
    179     /**
    180      * Center n/e coordinate of the desired screen center.
    181      */
    182     protected EastNorth center = calculateDefaultCenter();
    183213
    184214    private final transient Object paintRequestLock = new Object();
    185215    private Rectangle paintRect = null;
    public class NavigatableComponent extends JComponent implements Helpful {  
    187217
    188218    protected transient ViewportData initialViewport;
    189219
     220    private final NavigationModel navigationModel;
     221
     222    private transient final NavigationModel.ZoomChangeListener weakZoomListener = new WeakZoomChangeListener(this);
     223
    190224    protected transient final NavigationCursorManager cursorManager = new NavigationCursorManager(this);
    191225
    192226    /**
    193      * Constructs a new {@code NavigatableComponent}.
     227     * Constructs a new {@code NavigatableComponent} using the static default {@link NavigationModel} and zooming to the current bounds,
    194228     */
    195229    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;
    196243        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;
    197254    }
    198255
    199256    protected DataSet getCurrentDataSet() {
    public class NavigatableComponent extends JComponent implements Helpful {  
    257314    }
    258315
    259316    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);
    265318    }
    266319
    267320    /**
    public class NavigatableComponent extends JComponent implements Helpful {  
    269322     *      change the center by accessing the return value. Use zoomTo instead.
    270323     */
    271324    public EastNorth getCenter() {
    272         return center;
     325        return navigationModel.getCenter();
    273326    }
    274327
    275328    public double getScale() {
    276         return scale;
     329        return navigationModel.getScale();
    277330    }
    278331
    279332    /**
    public class NavigatableComponent extends JComponent implements Helpful {  
    283336     * @return Geographic coordinates from a specific pixel coordination on the screen.
    284337     */
    285338    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);
    289340    }
    290341
    291342    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));
    299344    }
    300345
    301346    /* FIXME: replace with better method - used by MapSlider */
    public class NavigatableComponent extends JComponent implements Helpful {  
    308353    /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
    309354    public Bounds getRealBounds() {
    310355        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)));
    317358    }
    318359
    319360    /**
    public class NavigatableComponent extends JComponent implements Helpful {  
    324365     *      on the screen.
    325366     */
    326367    public LatLon getLatLon(int x, int y) {
    327         return getProjection().eastNorth2latlon(getEastNorth(x, y));
     368        return navigationModel.getLatLon(new Point2D.Double(x, y));
    328369    }
    329370
    330371    public LatLon getLatLon(double x, double y) {
    331         return getLatLon((int) x, (int) y);
     372        return navigationModel.getLatLon(new Point2D.Double(x, y));
    332373    }
    333374
    334375    /**
    public class NavigatableComponent extends JComponent implements Helpful {  
    350391        double deltaNorth = (northMax - northMin) / 10;
    351392
    352393        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)));
    357398        }
    358399
    359400        return result;
    360401    }
    361402
    362403    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();
    365405    }
    366406
    367407    /**
    public class NavigatableComponent extends JComponent implements Helpful {  
    371411     *      to the own top/left.
    372412     */
    373413    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);
    379415    }
    380416
    381417    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);
    388419    }
    389420
    390421    public Point2D getPoint2D(Node n) {
    public class NavigatableComponent extends JComponent implements Helpful {  
    430461     * @param initial true if this call initializes the viewport.
    431462     */
    432463    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);
    512465    }
    513466
    514467    public void zoomTo(EastNorth newCenter) {
    515         zoomTo(newCenter, scale);
     468        zoomTo(newCenter, navigationModel.getScale());
    516469    }
    517470
    518471    public void zoomTo(LatLon newCenter) {
    public class NavigatableComponent extends JComponent implements Helpful {  
    529482     */
    530483    public void smoothScrollTo(EastNorth newCenter) {
    531484        // 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);
    556486    }
    557487
    558488    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);
    566490    }
    567491
    568492    public void zoomToFactor(EastNorth newCenter, double factor) {
    569         zoomTo(newCenter, scale*factor);
     493        zoomTo(newCenter, getScale()*factor);
    570494    }
    571495
    572496    public void zoomToFactor(double factor) {
    573         zoomTo(center, scale*factor);
     497        zoomTo(getCenter(), getScale()*factor);
    574498    }
    575499
    576500    public void zoomTo(ProjectionBounds box) {
    public class NavigatableComponent extends JComponent implements Helpful {  
    624548        zoomTo(box.getBounds());
    625549    }
    626550
    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 
    661551    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();
    667553    }
    668554
    669555    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();
    675557    }
    676558
    677559    public boolean hasZoomUndoEntries() {
    678         return !zoomUndoBuffer.isEmpty();
     560        return navigationModel.hasPreviousZoomEntries();
    679561    }
    680562
    681563    public boolean hasZoomRedoEntries() {
    682         return !zoomRedoBuffer.isEmpty();
     564        return navigationModel.hasNextZoomEntries();
    683565    }
    684566
    685567    private BBox getBBox(Point p, int snapDistance) {
    public class NavigatableComponent extends JComponent implements Helpful {  
    14311313     * @return A unique ID, as long as viewport dimensions are the same
    14321314     */
    14331315    public int getViewID() {
    1434         String x = center.east() + "_" + center.north() + "_" + scale + "_" +
     1316        String x = getCenter().east() + "_" + getCenter().north() + "_" + getScale() + "_" +
    14351317                getWidth() + "_" + getHeight() + "_" + getProjection().toString();
    14361318        CRC32 id = new CRC32();
    14371319        id.update(x.getBytes(StandardCharsets.UTF_8));
    public class NavigatableComponent extends JComponent implements Helpful {  
    14651347    }
    14661348
    14671349    @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
    14681368    public void paint(Graphics g) {
    14691369        synchronized (paintRequestLock) {
    14701370            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.
     2package org.openstreetmap.josm.gui.navigate;
     3
     4import java.awt.Component;
     5import java.awt.Dimension;
     6import java.awt.Point;
     7import java.awt.event.ComponentAdapter;
     8import java.awt.event.ComponentEvent;
     9import java.awt.geom.AffineTransform;
     10import java.awt.geom.Point2D;
     11import java.lang.ref.WeakReference;
     12import java.util.Date;
     13import java.util.Stack;
     14import java.util.Timer;
     15import java.util.TimerTask;
     16import java.util.concurrent.CopyOnWriteArrayList;
     17
     18import org.openstreetmap.josm.Main;
     19import org.openstreetmap.josm.data.Bounds;
     20import org.openstreetmap.josm.data.coor.CachedLatLon;
     21import org.openstreetmap.josm.data.coor.EastNorth;
     22import org.openstreetmap.josm.data.coor.LatLon;
     23import org.openstreetmap.josm.data.projection.Projection;
     24import org.openstreetmap.josm.data.projection.Projections;
     25import org.openstreetmap.josm.gui.util.GuiHelper;
     26import 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 */
     34public 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  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm;
    33
     4import static org.junit.Assert.assertEquals;
    45import static org.junit.Assert.fail;
    56
     7import java.awt.geom.Point2D;
    68import java.util.Arrays;
    79import java.util.Comparator;
    810
     11import org.openstreetmap.josm.data.coor.EastNorth;
     12import org.openstreetmap.josm.data.coor.LatLon;
     13
    914/**
    1015 * Various utils, useful for unit tests.
    1116 */
    public final class TestUtils {  
    106111        .append("\nCompared\no2: ").append(o2).append("\no3: ").append(o3).append("\ngave: ").append(d)
    107112        .toString();
    108113    }
     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    }
    109145}
  • 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.
     2package org.openstreetmap.josm.gui;
     3
     4import static org.junit.Assert.assertEquals;
     5import static org.openstreetmap.josm.TestUtils.assertEastNorthEquals;
     6import static org.openstreetmap.josm.TestUtils.assertLatLonEquals;
     7import static org.openstreetmap.josm.TestUtils.assertPointEquals;
     8
     9import java.awt.Rectangle;
     10import java.awt.geom.Point2D;
     11
     12import org.junit.Before;
     13import org.junit.BeforeClass;
     14import org.junit.Test;
     15import org.openstreetmap.josm.JOSMFixture;
     16import org.openstreetmap.josm.Main;
     17import org.openstreetmap.josm.data.Bounds;
     18import org.openstreetmap.josm.data.ProjectionBounds;
     19import org.openstreetmap.josm.data.coor.EastNorth;
     20import org.openstreetmap.josm.data.coor.LatLon;
     21import org.openstreetmap.josm.gui.util.GuiHelper;
     22
     23/**
     24 * Some tests for the {@link NavigatableComponent} class.
     25 * @author Michael Zangl
     26 *
     27 */
     28public 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}