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