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