001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.bbox;
003
004import java.awt.Point;
005import java.awt.event.ActionEvent;
006import java.awt.event.InputEvent;
007import java.awt.event.KeyEvent;
008import java.awt.event.MouseAdapter;
009import java.awt.event.MouseEvent;
010import java.util.Timer;
011import java.util.TimerTask;
012
013import javax.swing.AbstractAction;
014import javax.swing.ActionMap;
015import javax.swing.InputMap;
016import javax.swing.JComponent;
017import javax.swing.JPanel;
018import javax.swing.KeyStroke;
019
020import org.openstreetmap.josm.tools.PlatformManager;
021
022/**
023 * This class controls the user input by listening to mouse and key events.
024 * Currently implemented is: - zooming in and out with scrollwheel - zooming in
025 * and centering by double clicking - selecting an area by clicking and dragging
026 * the mouse
027 *
028 * @author Tim Haussmann
029 */
030public class SlippyMapControler extends MouseAdapter {
031
032    /** A Timer for smoothly moving the map area */
033    private static final Timer TIMER = new Timer(true);
034
035    /** Does the moving */
036    private MoveTask moveTask = new MoveTask();
037
038    /** How often to do the moving (milliseconds) */
039    private static long timerInterval = 20;
040
041    /** The maximum speed (pixels per timer interval) */
042    private static final double MAX_SPEED = 20;
043
044    /** The speed increase per timer interval when a cursor button is clicked */
045    private static final double ACCELERATION = 0.10;
046
047    private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK;
048
049    private static final String[] N = {
050            ",", ".", "up", "right", "down", "left"};
051    private static final int[] K = {
052            KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN, KeyEvent.VK_LEFT};
053
054    // start and end point of selection rectangle
055    private Point iStartSelectionPoint;
056    private Point iEndSelectionPoint;
057
058    private final SlippyMapBBoxChooser iSlippyMapChooser;
059
060    private boolean isSelecting;
061
062    /**
063     * Constructs a new {@code SlippyMapControler}.
064     * @param navComp navigatable component
065     * @param contentPane content pane
066     */
067    public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane) {
068        iSlippyMapChooser = navComp;
069        iSlippyMapChooser.addMouseListener(this);
070        iSlippyMapChooser.addMouseMotionListener(this);
071
072        if (contentPane != null) {
073            for (int i = 0; i < N.length; ++i) {
074                contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
075                        KeyStroke.getKeyStroke(K[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + N[i]);
076            }
077        }
078        isSelecting = false;
079
080        InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
081        ActionMap actionMap = navComp.getActionMap();
082
083        // map moving
084        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT");
085        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT");
086        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP");
087        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN");
088        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY");
089        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY");
090        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY");
091        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY");
092
093        // zooming. To avoid confusion about which modifier key to use,
094        // we just add all keys left of the space bar
095        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN");
096        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN");
097        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN");
098        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN");
099        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0, false), "ZOOM_IN");
100        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0, false), "ZOOM_IN");
101        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.SHIFT_DOWN_MASK, false), "ZOOM_IN");
102        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_OUT");
103        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), "ZOOM_OUT");
104        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), "ZOOM_OUT");
105        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0, false), "ZOOM_OUT");
106        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0, false), "ZOOM_OUT");
107
108        // action mapping
109        actionMap.put("MOVE_RIGHT", new MoveXAction(1));
110        actionMap.put("MOVE_LEFT", new MoveXAction(-1));
111        actionMap.put("MOVE_UP", new MoveYAction(-1));
112        actionMap.put("MOVE_DOWN", new MoveYAction(1));
113        actionMap.put("STOP_MOVE_HORIZONTALLY", new MoveXAction(0));
114        actionMap.put("STOP_MOVE_VERTICALLY", new MoveYAction(0));
115        actionMap.put("ZOOM_IN", new ZoomInAction());
116        actionMap.put("ZOOM_OUT", new ZoomOutAction());
117    }
118
119    /**
120     * Start drawing the selection rectangle if it was the 1st button (left button)
121     */
122    @Override
123    public void mousePressed(MouseEvent e) {
124        if (e.getButton() == MouseEvent.BUTTON1 && !(PlatformManager.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
125            iStartSelectionPoint = e.getPoint();
126            iEndSelectionPoint = e.getPoint();
127        }
128    }
129
130    @Override
131    public void mouseDragged(MouseEvent e) {
132        if (iStartSelectionPoint != null && (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK
133                && !(PlatformManager.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
134            iEndSelectionPoint = e.getPoint();
135            iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint);
136            isSelecting = true;
137        }
138    }
139
140    /**
141     * When dragging the map change the cursor back to it's pre-move cursor. If
142     * a double-click occurs center and zoom the map on the clicked location.
143     */
144    @Override
145    public void mouseReleased(MouseEvent e) {
146        if (e.getButton() == MouseEvent.BUTTON1) {
147
148            if (isSelecting && e.getClickCount() == 1) {
149                iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint());
150
151                // reset the selections start and end
152                iEndSelectionPoint = null;
153                iStartSelectionPoint = null;
154                isSelecting = false;
155
156            } else {
157                iSlippyMapChooser.handleAttribution(e.getPoint(), true);
158            }
159        }
160    }
161
162    @Override
163    public void mouseMoved(MouseEvent e) {
164        iSlippyMapChooser.handleAttribution(e.getPoint(), false);
165    }
166
167    private class MoveXAction extends AbstractAction {
168
169        private final int direction;
170
171        MoveXAction(int direction) {
172            this.direction = direction;
173        }
174
175        @Override
176        public void actionPerformed(ActionEvent e) {
177            moveTask.setDirectionX(direction);
178        }
179    }
180
181    private class MoveYAction extends AbstractAction {
182
183        private final int direction;
184
185        MoveYAction(int direction) {
186            this.direction = direction;
187        }
188
189        @Override
190        public void actionPerformed(ActionEvent e) {
191            moveTask.setDirectionY(direction);
192        }
193    }
194
195    /** Moves the map depending on which cursor keys are pressed (or not) */
196    private class MoveTask extends TimerTask {
197        /** The current x speed (pixels per timer interval) */
198        private double speedX = 1;
199
200        /** The current y speed (pixels per timer interval) */
201        private double speedY = 1;
202
203        /** The horizontal direction of movement, -1:left, 0:stop, 1:right */
204        private int directionX;
205
206        /** The vertical direction of movement, -1:up, 0:stop, 1:down */
207        private int directionY;
208
209        /**
210         * Indicated if <code>moveTask</code> is currently enabled (periodically
211         * executed via timer) or disabled
212         */
213        protected boolean scheduled;
214
215        protected void setDirectionX(int directionX) {
216            this.directionX = directionX;
217            updateScheduleStatus();
218        }
219
220        protected void setDirectionY(int directionY) {
221            this.directionY = directionY;
222            updateScheduleStatus();
223        }
224
225        private void updateScheduleStatus() {
226            boolean newMoveTaskState = !(directionX == 0 && directionY == 0);
227
228            if (newMoveTaskState != scheduled) {
229                scheduled = newMoveTaskState;
230                if (newMoveTaskState) {
231                    TIMER.schedule(this, 0, timerInterval);
232                } else {
233                    // We have to create a new instance because rescheduling a
234                    // once canceled TimerTask is not possible
235                    moveTask = new MoveTask();
236                    cancel(); // Stop this TimerTask
237                }
238            }
239        }
240
241        @Override
242        public void run() {
243            // update the x speed
244            switch (directionX) {
245            case -1:
246                if (speedX > -1) {
247                    speedX = -1;
248                }
249                if (speedX > -1 * MAX_SPEED) {
250                    speedX -= ACCELERATION;
251                }
252                break;
253            case 0:
254                speedX = 0;
255                break;
256            case 1:
257                if (speedX < 1) {
258                    speedX = 1;
259                }
260                if (speedX < MAX_SPEED) {
261                    speedX += ACCELERATION;
262                }
263                break;
264            default:
265                throw new IllegalStateException(Integer.toString(directionX));
266            }
267
268            // update the y speed
269            switch (directionY) {
270            case -1:
271                if (speedY > -1) {
272                    speedY = -1;
273                }
274                if (speedY > -1 * MAX_SPEED) {
275                    speedY -= ACCELERATION;
276                }
277                break;
278            case 0:
279                speedY = 0;
280                break;
281            case 1:
282                if (speedY < 1) {
283                    speedY = 1;
284                }
285                if (speedY < MAX_SPEED) {
286                    speedY += ACCELERATION;
287                }
288                break;
289            default:
290                throw new IllegalStateException(Integer.toString(directionY));
291            }
292
293            // move the map
294            int moveX = (int) Math.floor(speedX);
295            int moveY = (int) Math.floor(speedY);
296            if (moveX != 0 || moveY != 0) {
297                iSlippyMapChooser.moveMap(moveX, moveY);
298            }
299        }
300    }
301
302    private class ZoomInAction extends AbstractAction {
303
304        @Override
305        public void actionPerformed(ActionEvent e) {
306            iSlippyMapChooser.zoomIn();
307        }
308    }
309
310    private class ZoomOutAction extends AbstractAction {
311
312        @Override
313        public void actionPerformed(ActionEvent e) {
314            iSlippyMapChooser.zoomOut();
315        }
316    }
317}