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