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     * @param navComp navigatable component
067     * @param contentPane content pane
068     */
069    public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane) {
070        iSlippyMapChooser = navComp;
071        iSlippyMapChooser.addMouseListener(this);
072        iSlippyMapChooser.addMouseMotionListener(this);
073
074        if (contentPane != null) {
075            for (int i = 0; i < N.length; ++i) {
076                contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
077                        KeyStroke.getKeyStroke(K[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + N[i]);
078            }
079        }
080        isSelecting = false;
081
082        InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
083        ActionMap actionMap = navComp.getActionMap();
084
085        // map moving
086        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT");
087        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT");
088        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP");
089        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN");
090        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY");
091        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY");
092        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY");
093        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY");
094
095        // zooming. To avoid confusion about which modifier key to use,
096        // we just add all keys left of the space bar
097        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN");
098        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN");
099        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN");
100        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN");
101        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0, false), "ZOOM_IN");
102        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0, false), "ZOOM_IN");
103        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.SHIFT_DOWN_MASK, false), "ZOOM_IN");
104        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_OUT");
105        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), "ZOOM_OUT");
106        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), "ZOOM_OUT");
107        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0, false), "ZOOM_OUT");
108        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0, false), "ZOOM_OUT");
109
110        // action mapping
111        actionMap.put("MOVE_RIGHT", new MoveXAction(1));
112        actionMap.put("MOVE_LEFT", new MoveXAction(-1));
113        actionMap.put("MOVE_UP", new MoveYAction(-1));
114        actionMap.put("MOVE_DOWN", new MoveYAction(1));
115        actionMap.put("STOP_MOVE_HORIZONTALLY", new MoveXAction(0));
116        actionMap.put("STOP_MOVE_VERTICALLY", new MoveYAction(0));
117        actionMap.put("ZOOM_IN", new ZoomInAction());
118        actionMap.put("ZOOM_OUT", new ZoomOutAction());
119    }
120
121    /**
122     * Start drawing the selection rectangle if it was the 1st button (left
123     * button)
124     */
125    @Override
126    public void mousePressed(MouseEvent e) {
127        if (e.getButton() == MouseEvent.BUTTON1 && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
128            iStartSelectionPoint = e.getPoint();
129            iEndSelectionPoint = e.getPoint();
130        }
131    }
132
133    @Override
134    public void mouseDragged(MouseEvent e) {
135        if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK &&
136                !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
137            if (iStartSelectionPoint != null) {
138                iEndSelectionPoint = e.getPoint();
139                iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint);
140                isSelecting = true;
141            }
142        }
143    }
144
145    /**
146     * When dragging the map change the cursor back to it's pre-move cursor. If
147     * a double-click occurs center and zoom the map on the clicked location.
148     */
149    @Override
150    public void mouseReleased(MouseEvent e) {
151        if (e.getButton() == MouseEvent.BUTTON1) {
152
153            if (isSelecting && e.getClickCount() == 1) {
154                iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint());
155
156                // reset the selections start and end
157                iEndSelectionPoint = null;
158                iStartSelectionPoint = null;
159                isSelecting = false;
160
161            } else {
162                iSlippyMapChooser.handleAttribution(e.getPoint(), true);
163            }
164        }
165    }
166
167    @Override
168    public void mouseMoved(MouseEvent e) {
169        iSlippyMapChooser.handleAttribution(e.getPoint(), false);
170    }
171
172    private class MoveXAction extends AbstractAction {
173
174        private final int direction;
175
176        MoveXAction(int direction) {
177            this.direction = direction;
178        }
179
180        @Override
181        public void actionPerformed(ActionEvent e) {
182            moveTask.setDirectionX(direction);
183        }
184    }
185
186    private class MoveYAction extends AbstractAction {
187
188        private final int direction;
189
190        MoveYAction(int direction) {
191            this.direction = direction;
192        }
193
194        @Override
195        public void actionPerformed(ActionEvent e) {
196            moveTask.setDirectionY(direction);
197        }
198    }
199
200    /** Moves the map depending on which cursor keys are pressed (or not) */
201    private class MoveTask extends TimerTask {
202        /** The current x speed (pixels per timer interval) */
203        private double speedX = 1;
204
205        /** The current y speed (pixels per timer interval) */
206        private double speedY = 1;
207
208        /** The horizontal direction of movement, -1:left, 0:stop, 1:right */
209        private int directionX;
210
211        /** The vertical direction of movement, -1:up, 0:stop, 1:down */
212        private int directionY;
213
214        /**
215         * Indicated if <code>moveTask</code> is currently enabled (periodically
216         * executed via timer) or disabled
217         */
218        protected boolean scheduled;
219
220        protected void setDirectionX(int directionX) {
221            this.directionX = directionX;
222            updateScheduleStatus();
223        }
224
225        protected void setDirectionY(int directionY) {
226            this.directionY = directionY;
227            updateScheduleStatus();
228        }
229
230        private void updateScheduleStatus() {
231            boolean newMoveTaskState = !(directionX == 0 && directionY == 0);
232
233            if (newMoveTaskState != scheduled) {
234                scheduled = newMoveTaskState;
235                if (newMoveTaskState) {
236                    timer.schedule(this, 0, timerInterval);
237                } else {
238                    // We have to create a new instance because rescheduling a
239                    // once canceled TimerTask is not possible
240                    moveTask = new MoveTask();
241                    cancel(); // Stop this TimerTask
242                }
243            }
244        }
245
246        @Override
247        public void run() {
248            // update the x speed
249            switch (directionX) {
250            case -1:
251                if (speedX > -1) {
252                    speedX = -1;
253                }
254                if (speedX > -1 * MAX_SPEED) {
255                    speedX -= ACCELERATION;
256                }
257                break;
258            case 0:
259                speedX = 0;
260                break;
261            case 1:
262                if (speedX < 1) {
263                    speedX = 1;
264                }
265                if (speedX < MAX_SPEED) {
266                    speedX += ACCELERATION;
267                }
268                break;
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            }
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}