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.awt.event.MouseListener;
011import java.awt.event.MouseMotionListener;
012import java.util.Timer;
013import java.util.TimerTask;
014
015import javax.swing.AbstractAction;
016import javax.swing.ActionMap;
017import javax.swing.InputMap;
018import javax.swing.JComponent;
019import javax.swing.JPanel;
020import javax.swing.KeyStroke;
021
022import org.openstreetmap.josm.Main;
023
024/**
025 * This class controls the user input by listening to mouse and key events.
026 * Currently implemented is: - zooming in and out with scrollwheel - zooming in
027 * and centering by double clicking - selecting an area by clicking and dragging
028 * the mouse
029 *
030 * @author Tim Haussmann
031 */
032public class SlippyMapControler extends MouseAdapter implements MouseMotionListener, MouseListener {
033
034    /** A Timer for smoothly moving the map area */
035    private static final Timer timer = new Timer(true);
036
037    /** Does the moving */
038    private MoveTask moveTask = new MoveTask();
039
040    /** How often to do the moving (milliseconds) */
041    private static long timerInterval = 20;
042
043    /** The maximum speed (pixels per timer interval) */
044    private static final double MAX_SPEED = 20;
045
046    /** The speed increase per timer interval when a cursor button is clicked */
047    private static final double ACCELERATION = 0.10;
048
049    private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK;
050
051    private static final String[] N = {
052            ",", ".", "up", "right", "down", "left"};
053    private static final int[] K = {
054            KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN, KeyEvent.VK_LEFT};
055
056    // start and end point of selection rectangle
057    private Point iStartSelectionPoint;
058    private Point iEndSelectionPoint;
059
060    private final SlippyMapBBoxChooser iSlippyMapChooser;
061
062    private boolean isSelecting;
063
064    /**
065     * Constructs a new {@code SlippyMapControler}.
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 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 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            }
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            }
291
292            // move the map
293            int moveX = (int) Math.floor(speedX);
294            int moveY = (int) Math.floor(speedY);
295            if (moveX != 0 || moveY != 0) {
296                iSlippyMapChooser.moveMap(moveX, moveY);
297            }
298        }
299    }
300
301    private class ZoomInAction extends AbstractAction {
302
303        @Override
304        public void actionPerformed(ActionEvent e) {
305            iSlippyMapChooser.zoomIn();
306        }
307    }
308
309    private class ZoomOutAction extends AbstractAction {
310
311        @Override
312        public void actionPerformed(ActionEvent e) {
313            iSlippyMapChooser.zoomOut();
314        }
315    }
316}