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.Main;
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
121     * button)
122     */
123    @Override
124    public void mousePressed(MouseEvent e) {
125        if (e.getButton() == MouseEvent.BUTTON1 && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
126            iStartSelectionPoint = e.getPoint();
127            iEndSelectionPoint = e.getPoint();
128        }
129    }
130
131    @Override
132    public void mouseDragged(MouseEvent e) {
133        if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK &&
134                !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
135            if (iStartSelectionPoint != null) {
136                iEndSelectionPoint = e.getPoint();
137                iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint);
138                isSelecting = true;
139            }
140        }
141    }
142
143    /**
144     * When dragging the map change the cursor back to it's pre-move cursor. If
145     * a double-click occurs center and zoom the map on the clicked location.
146     */
147    @Override
148    public void mouseReleased(MouseEvent e) {
149        if (e.getButton() == MouseEvent.BUTTON1) {
150
151            if (isSelecting && e.getClickCount() == 1) {
152                iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint());
153
154                // reset the selections start and end
155                iEndSelectionPoint = null;
156                iStartSelectionPoint = null;
157                isSelecting = false;
158
159            } else {
160                iSlippyMapChooser.handleAttribution(e.getPoint(), true);
161            }
162        }
163    }
164
165    @Override
166    public void mouseMoved(MouseEvent e) {
167        iSlippyMapChooser.handleAttribution(e.getPoint(), false);
168    }
169
170    private class MoveXAction extends AbstractAction {
171
172        private final int direction;
173
174        MoveXAction(int direction) {
175            this.direction = direction;
176        }
177
178        @Override
179        public void actionPerformed(ActionEvent e) {
180            moveTask.setDirectionX(direction);
181        }
182    }
183
184    private class MoveYAction extends AbstractAction {
185
186        private final int direction;
187
188        MoveYAction(int direction) {
189            this.direction = direction;
190        }
191
192        @Override
193        public void actionPerformed(ActionEvent e) {
194            moveTask.setDirectionY(direction);
195        }
196    }
197
198    /** Moves the map depending on which cursor keys are pressed (or not) */
199    private class MoveTask extends TimerTask {
200        /** The current x speed (pixels per timer interval) */
201        private double speedX = 1;
202
203        /** The current y speed (pixels per timer interval) */
204        private double speedY = 1;
205
206        /** The horizontal direction of movement, -1:left, 0:stop, 1:right */
207        private int directionX;
208
209        /** The vertical direction of movement, -1:up, 0:stop, 1:down */
210        private int directionY;
211
212        /**
213         * Indicated if <code>moveTask</code> is currently enabled (periodically
214         * executed via timer) or disabled
215         */
216        protected boolean scheduled;
217
218        protected void setDirectionX(int directionX) {
219            this.directionX = directionX;
220            updateScheduleStatus();
221        }
222
223        protected void setDirectionY(int directionY) {
224            this.directionY = directionY;
225            updateScheduleStatus();
226        }
227
228        private void updateScheduleStatus() {
229            boolean newMoveTaskState = !(directionX == 0 && directionY == 0);
230
231            if (newMoveTaskState != scheduled) {
232                scheduled = newMoveTaskState;
233                if (newMoveTaskState) {
234                    timer.schedule(this, 0, timerInterval);
235                } else {
236                    // We have to create a new instance because rescheduling a
237                    // once canceled TimerTask is not possible
238                    moveTask = new MoveTask();
239                    cancel(); // Stop this TimerTask
240                }
241            }
242        }
243
244        @Override
245        public void run() {
246            // update the x speed
247            switch (directionX) {
248            case -1:
249                if (speedX > -1) {
250                    speedX = -1;
251                }
252                if (speedX > -1 * MAX_SPEED) {
253                    speedX -= ACCELERATION;
254                }
255                break;
256            case 0:
257                speedX = 0;
258                break;
259            case 1:
260                if (speedX < 1) {
261                    speedX = 1;
262                }
263                if (speedX < MAX_SPEED) {
264                    speedX += ACCELERATION;
265                }
266                break;
267            default:
268                throw new IllegalStateException(Integer.toString(directionX));
269            }
270
271            // update the y speed
272            switch (directionY) {
273            case -1:
274                if (speedY > -1) {
275                    speedY = -1;
276                }
277                if (speedY > -1 * MAX_SPEED) {
278                    speedY -= ACCELERATION;
279                }
280                break;
281            case 0:
282                speedY = 0;
283                break;
284            case 1:
285                if (speedY < 1) {
286                    speedY = 1;
287                }
288                if (speedY < MAX_SPEED) {
289                    speedY += ACCELERATION;
290                }
291                break;
292            default:
293                throw new IllegalStateException(Integer.toString(directionY));
294            }
295
296            // move the map
297            int moveX = (int) Math.floor(speedX);
298            int moveY = (int) Math.floor(speedY);
299            if (moveX != 0 || moveY != 0) {
300                iSlippyMapChooser.moveMap(moveX, moveY);
301            }
302        }
303    }
304
305    private class ZoomInAction extends AbstractAction {
306
307        @Override
308        public void actionPerformed(ActionEvent e) {
309            iSlippyMapChooser.zoomIn();
310        }
311    }
312
313    private class ZoomOutAction extends AbstractAction {
314
315        @Override
316        public void actionPerformed(ActionEvent e) {
317            iSlippyMapChooser.zoomOut();
318        }
319    }
320}