001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Cursor;
007import java.awt.Point;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.awt.event.MouseAdapter;
011import java.awt.event.MouseEvent;
012import java.awt.event.MouseMotionListener;
013import java.awt.event.MouseWheelEvent;
014import java.awt.event.MouseWheelListener;
015
016import javax.swing.AbstractAction;
017import javax.swing.ActionMap;
018import javax.swing.InputMap;
019import javax.swing.JComponent;
020import javax.swing.JPanel;
021import javax.swing.KeyStroke;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.data.coor.EastNorth;
025import org.openstreetmap.josm.tools.Destroyable;
026import org.openstreetmap.josm.tools.PlatformHookOsx;
027import org.openstreetmap.josm.tools.Shortcut;
028
029/**
030 * Enables moving of the map by holding down the right mouse button and drag
031 * the mouse. Also, enables zooming by the mouse wheel.
032 *
033 * @author imi
034 */
035public class MapMover extends MouseAdapter implements MouseMotionListener, MouseWheelListener, Destroyable {
036
037    private final class ZoomerAction extends AbstractAction {
038        private final String action;
039        public ZoomerAction(String action) {
040            this.action = action;
041        }
042        @Override
043        public void actionPerformed(ActionEvent e) {
044            if (action.equals(".") || action.equals(",")) {
045                Point mouse = nc.getMousePosition();
046                if (mouse == null)
047                    mouse = new Point((int)nc.getBounds().getCenterX(), (int)nc.getBounds().getCenterY());
048                MouseWheelEvent we = new MouseWheelEvent(nc, e.getID(), e.getWhen(), e.getModifiers(), mouse.x, mouse.y, 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, 1, action.equals(",") ? -1 : 1);
049                mouseWheelMoved(we);
050            } else {
051                EastNorth center = nc.getCenter();
052                EastNorth newcenter = nc.getEastNorth(nc.getWidth()/2+nc.getWidth()/5, nc.getHeight()/2+nc.getHeight()/5);
053                if (action.equals("left"))
054                    nc.zoomTo(new EastNorth(2*center.east()-newcenter.east(), center.north()));
055                else if (action.equals("right"))
056                    nc.zoomTo(new EastNorth(newcenter.east(), center.north()));
057                else if (action.equals("up"))
058                    nc.zoomTo(new EastNorth(center.east(), 2*center.north()-newcenter.north()));
059                else if (action.equals("down"))
060                    nc.zoomTo(new EastNorth(center.east(), newcenter.north()));
061            }
062        }
063    }
064
065    /**
066     * The point in the map that was the under the mouse point
067     * when moving around started.
068     */
069    private EastNorth mousePosMove;
070    /**
071     * The map to move around.
072     */
073    private final NavigatableComponent nc;
074    private final JPanel contentPane;
075
076    private boolean movementInPlace = false;
077
078    /**
079     * Create a new MapMover
080     */
081    public MapMover(NavigatableComponent navComp, JPanel contentPane) {
082        this.nc = navComp;
083        this.contentPane = contentPane;
084        nc.addMouseListener(this);
085        nc.addMouseMotionListener(this);
086        nc.addMouseWheelListener(this);
087
088        if (contentPane != null) {
089            contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
090                Shortcut.registerShortcut("system:movefocusright", tr("Map: {0}", tr("Move right")), KeyEvent.VK_RIGHT, Shortcut.CTRL).getKeyStroke(),
091                "MapMover.Zoomer.right");
092            contentPane.getActionMap().put("MapMover.Zoomer.right", new ZoomerAction("right"));
093
094            contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
095                Shortcut.registerShortcut("system:movefocusleft", tr("Map: {0}", tr("Move left")), KeyEvent.VK_LEFT, Shortcut.CTRL).getKeyStroke(),
096                "MapMover.Zoomer.left");
097            contentPane.getActionMap().put("MapMover.Zoomer.left", new ZoomerAction("left"));
098
099            contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
100                Shortcut.registerShortcut("system:movefocusup", tr("Map: {0}", tr("Move up")), KeyEvent.VK_UP, Shortcut.CTRL).getKeyStroke(),
101                "MapMover.Zoomer.up");
102            contentPane.getActionMap().put("MapMover.Zoomer.up", new ZoomerAction("up"));
103
104            contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
105                Shortcut.registerShortcut("system:movefocusdown", tr("Map: {0}", tr("Move down")), KeyEvent.VK_DOWN, Shortcut.CTRL).getKeyStroke(),
106                "MapMover.Zoomer.down");
107            contentPane.getActionMap().put("MapMover.Zoomer.down", new ZoomerAction("down"));
108
109            contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
110                Shortcut.registerShortcut("view:zoominalternate", tr("Map: {0}", tr("Zoom in")), KeyEvent.VK_COMMA, Shortcut.CTRL).getKeyStroke(),
111                "MapMover.Zoomer.in");
112            contentPane.getActionMap().put("MapMover.Zoomer.in", new ZoomerAction(","));
113
114            contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
115                Shortcut.registerShortcut("view:zoomoutalternate", tr("Map: {0}", tr("Zoom out")), KeyEvent.VK_PERIOD, Shortcut.CTRL).getKeyStroke(),
116                "MapMover.Zoomer.out");
117            contentPane.getActionMap().put("MapMover.Zoomer.out", new ZoomerAction("."));
118        }
119    }
120
121    /**
122     * If the right (and only the right) mouse button is pressed, move the map
123     */
124    @Override
125    public void mouseDragged(MouseEvent e) {
126        int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK;
127        if ((e.getModifiersEx() & (MouseEvent.BUTTON3_DOWN_MASK | offMask)) == MouseEvent.BUTTON3_DOWN_MASK) {
128            if (mousePosMove == null)
129                startMovement(e);
130            EastNorth center = nc.getCenter();
131            EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY());
132            nc.zoomTo(new EastNorth(
133                    mousePosMove.east() + center.east() - mouseCenter.east(),
134                    mousePosMove.north() + center.north() - mouseCenter.north()));
135        } else
136            endMovement();
137    }
138
139    /**
140     * Start the movement, if it was the 3rd button (right button).
141     */
142    @Override public void mousePressed(MouseEvent e) {
143        int offMask = MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK;
144        int macMouseMask = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK;
145        if (e.getButton() == MouseEvent.BUTTON3 && (e.getModifiersEx() & offMask) == 0) {
146            startMovement(e);
147        } else if (isPlatformOsx() && e.getModifiersEx() == macMouseMask) {
148            startMovement(e);
149        }
150    }
151
152    /**
153     * Change the cursor back to it's pre-move cursor.
154     */
155    @Override public void mouseReleased(MouseEvent e) {
156        if (e.getButton() == MouseEvent.BUTTON3) {
157            endMovement();
158        } else if (isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1) {
159            endMovement();
160        }
161    }
162
163    /**
164     * Start movement by setting a new cursor and remember the current mouse
165     * position.
166     * @param e The mouse event that leat to the movement from.
167     */
168    private void startMovement(MouseEvent e) {
169        if (movementInPlace)
170            return;
171        movementInPlace = true;
172        mousePosMove = nc.getEastNorth(e.getX(), e.getY());
173        nc.setNewCursor(Cursor.MOVE_CURSOR, this);
174    }
175
176    /**
177     * End the movement. Setting back the cursor and clear the movement variables
178     */
179    private void endMovement() {
180        if (!movementInPlace)
181            return;
182        movementInPlace = false;
183        nc.resetCursor(this);
184        mousePosMove = null;
185    }
186
187    /**
188     * Zoom the map by 1/5th of current zoom per wheel-delta.
189     * @param e The wheel event.
190     */
191    @Override
192    public void mouseWheelMoved(MouseWheelEvent e) {
193        nc.zoomToFactor(e.getX(), e.getY(), Math.pow(Math.sqrt(2), e.getWheelRotation()));
194    }
195
196    /**
197     * Emulates dragging on Mac OSX
198     */
199    @Override
200    public void mouseMoved(MouseEvent e) {
201        if (!movementInPlace)
202            return;
203        // Mac OSX simulates with  ctrl + mouse 1  the second mouse button hence no dragging events get fired.
204        // Is only the selected mouse button pressed?
205        if (isPlatformOsx()) {
206            if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) {
207                if (mousePosMove == null) {
208                    startMovement(e);
209                }
210                EastNorth center = nc.getCenter();
211                EastNorth mouseCenter = nc.getEastNorth(e.getX(), e.getY());
212                nc.zoomTo(new EastNorth(mousePosMove.east() + center.east() - mouseCenter.east(), mousePosMove.north()
213                        + center.north() - mouseCenter.north()));
214            } else {
215                endMovement();
216            }
217        }
218    }
219
220    /**
221     * Replies true if we are currently running on OSX
222     *
223     * @return true if we are currently running on OSX
224     */
225    public static boolean isPlatformOsx() {
226        return Main.platform instanceof PlatformHookOsx;
227    }
228
229    @Override
230    public void destroy() {
231        if (this.contentPane != null) {
232            InputMap inputMap = contentPane.getInputMap();
233            KeyStroke[] inputKeys = inputMap.keys();
234            if (inputKeys != null) {
235                for (KeyStroke key : inputKeys) {
236                    Object binding = inputMap.get(key);
237                    if (binding instanceof String && ((String)binding).startsWith("MapMover.")) {
238                        inputMap.remove(key);
239                    }
240                }
241            }
242            ActionMap actionMap = contentPane.getActionMap();
243            Object[] actionsKeys = actionMap.keys();
244            if (actionsKeys != null) {
245                for (Object key : actionsKeys) {
246                    if (key instanceof String && ((String)key).startsWith("MapMover.")) {
247                        actionMap.remove(key);
248                    }
249                }
250            }
251        }
252    }
253}