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 SlippyMapController 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 final 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 SlippyMapController}.
064     * @param navComp navigable component
065     * @param contentPane content pane
066     */
067    public SlippyMapController(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.handleMouseMoved(e.getPoint());
165        iSlippyMapChooser.handleAttribution(e.getPoint(), false);
166    }
167
168    private class MoveXAction extends AbstractAction {
169
170        private final int direction;
171
172        MoveXAction(int direction) {
173            this.direction = direction;
174        }
175
176        @Override
177        public void actionPerformed(ActionEvent e) {
178            moveTask.setDirectionX(direction);
179        }
180    }
181
182    private class MoveYAction extends AbstractAction {
183
184        private final int direction;
185
186        MoveYAction(int direction) {
187            this.direction = direction;
188        }
189
190        @Override
191        public void actionPerformed(ActionEvent e) {
192            moveTask.setDirectionY(direction);
193        }
194    }
195
196    /** Moves the map depending on which cursor keys are pressed (or not) */
197    private class MoveTask extends TimerTask {
198        /** The current x speed (pixels per timer interval) */
199        private double speedX = 1;
200
201        /** The current y speed (pixels per timer interval) */
202        private double speedY = 1;
203
204        /** The horizontal direction of movement, -1:left, 0:stop, 1:right */
205        private int directionX;
206
207        /** The vertical direction of movement, -1:up, 0:stop, 1:down */
208        private int directionY;
209
210        /**
211         * Indicated if <code>moveTask</code> is currently enabled (periodically
212         * executed via timer) or disabled
213         */
214        protected boolean scheduled;
215
216        protected void setDirectionX(int directionX) {
217            this.directionX = directionX;
218            updateScheduleStatus();
219        }
220
221        protected void setDirectionY(int directionY) {
222            this.directionY = directionY;
223            updateScheduleStatus();
224        }
225
226        private void updateScheduleStatus() {
227            boolean newMoveTaskState = !(directionX == 0 && directionY == 0);
228
229            if (newMoveTaskState != scheduled) {
230                scheduled = newMoveTaskState;
231                if (newMoveTaskState) {
232                    TIMER.schedule(this, 0, timerInterval);
233                } else {
234                    // We have to create a new instance because rescheduling a
235                    // once canceled TimerTask is not possible
236                    moveTask = new MoveTask();
237                    cancel(); // Stop this TimerTask
238                }
239            }
240        }
241
242        @Override
243        public void run() {
244            // update the x speed
245            switch (directionX) {
246            case -1:
247                if (speedX > -1) {
248                    speedX = -1;
249                }
250                if (speedX > -1 * MAX_SPEED) {
251                    speedX -= ACCELERATION;
252                }
253                break;
254            case 0:
255                speedX = 0;
256                break;
257            case 1:
258                if (speedX < 1) {
259                    speedX = 1;
260                }
261                if (speedX < MAX_SPEED) {
262                    speedX += ACCELERATION;
263                }
264                break;
265            default:
266                throw new IllegalStateException(Integer.toString(directionX));
267            }
268
269            // update the y speed
270            switch (directionY) {
271            case -1:
272                if (speedY > -1) {
273                    speedY = -1;
274                }
275                if (speedY > -1 * MAX_SPEED) {
276                    speedY -= ACCELERATION;
277                }
278                break;
279            case 0:
280                speedY = 0;
281                break;
282            case 1:
283                if (speedY < 1) {
284                    speedY = 1;
285                }
286                if (speedY < MAX_SPEED) {
287                    speedY += ACCELERATION;
288                }
289                break;
290            default:
291                throw new IllegalStateException(Integer.toString(directionY));
292            }
293
294            // move the map
295            int moveX = (int) Math.floor(speedX);
296            int moveY = (int) Math.floor(speedY);
297            if (moveX != 0 || moveY != 0) {
298                iSlippyMapChooser.moveMap(moveX, moveY);
299            }
300        }
301    }
302
303    private class ZoomInAction extends AbstractAction {
304
305        @Override
306        public void actionPerformed(ActionEvent e) {
307            iSlippyMapChooser.zoomIn();
308        }
309    }
310
311    private class ZoomOutAction extends AbstractAction {
312
313        @Override
314        public void actionPerformed(ActionEvent e) {
315            iSlippyMapChooser.zoomOut();
316        }
317    }
318}