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