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;
021import org.openstreetmap.josm.Main;
022
023/**
024 * This class controls the user input by listening to mouse and key events.
025 * Currently implemented is: - zooming in and out with scrollwheel - zooming in
026 * and centering by double clicking - selecting an area by clicking and dragging
027 * the mouse
028 *
029 * @author Tim Haussmann
030 */
031public class SlippyMapControler extends MouseAdapter implements MouseMotionListener, MouseListener {
032
033    /** A Timer for smoothly moving the map area */
034    private static final Timer timer = new Timer(true);
035
036    /** Does the moving */
037    private MoveTask moveTask = new MoveTask();
038
039    /** How often to do the moving (milliseconds) */
040    private static long timerInterval = 20;
041
042    /** The maximum speed (pixels per timer interval) */
043    private static final double MAX_SPEED = 20;
044
045    /** The speed increase per timer interval when a cursor button is clicked */
046    private static final double ACCELERATION = 0.10;
047
048    private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK;
049
050    // start and end point of selection rectangle
051    private Point iStartSelectionPoint;
052    private Point iEndSelectionPoint;
053
054    private final SlippyMapBBoxChooser iSlippyMapChooser;
055
056    private boolean isSelecting;
057
058    /**
059     * Constructs a new {@code SlippyMapControler}.
060     */
061    public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane) {
062        iSlippyMapChooser = navComp;
063        iSlippyMapChooser.addMouseListener(this);
064        iSlippyMapChooser.addMouseMotionListener(this);
065
066        String[] n = { ",", ".", "up", "right", "down", "left" };
067        int[] k = { KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN,
068                KeyEvent.VK_LEFT };
069
070        if (contentPane != null) {
071            for (int i = 0; i < n.length; ++i) {
072                contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
073                        KeyStroke.getKeyStroke(k[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + n[i]);
074            }
075        }
076        isSelecting = false;
077
078        InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
079        ActionMap actionMap = navComp.getActionMap();
080
081        // map moving
082        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT");
083        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT");
084        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP");
085        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN");
086        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY");
087        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY");
088        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY");
089        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY");
090
091        // zooming. To avoid confusion about which modifier key to use,
092        // we just add all keys left of the space bar
093        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN");
094        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN");
095        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN");
096        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN");
097        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0, false), "ZOOM_IN");
098        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0, false), "ZOOM_IN");
099        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.SHIFT_DOWN_MASK, false), "ZOOM_IN");
100        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_OUT");
101        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), "ZOOM_OUT");
102        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), "ZOOM_OUT");
103        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0, false), "ZOOM_OUT");
104        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0, false), "ZOOM_OUT");
105
106        // action mapping
107        actionMap.put("MOVE_RIGHT", new MoveXAction(1));
108        actionMap.put("MOVE_LEFT", new MoveXAction(-1));
109        actionMap.put("MOVE_UP", new MoveYAction(-1));
110        actionMap.put("MOVE_DOWN", new MoveYAction(1));
111        actionMap.put("STOP_MOVE_HORIZONTALLY", new MoveXAction(0));
112        actionMap.put("STOP_MOVE_VERTICALLY", new MoveYAction(0));
113        actionMap.put("ZOOM_IN", new ZoomInAction());
114        actionMap.put("ZOOM_OUT", new ZoomOutAction());
115    }
116
117    /**
118     * Start drawing the selection rectangle if it was the 1st button (left
119     * button)
120     */
121    @Override
122    public void mousePressed(MouseEvent e) {
123        if (e.getButton() == MouseEvent.BUTTON1 && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
124            iStartSelectionPoint = e.getPoint();
125            iEndSelectionPoint = e.getPoint();
126        }
127    }
128
129    @Override
130    public void mouseDragged(MouseEvent e) {
131        if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK &&
132                !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
133            if (iStartSelectionPoint != null) {
134                iEndSelectionPoint = e.getPoint();
135                iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint);
136                isSelecting = true;
137            }
138        }
139    }
140
141    /**
142     * When dragging the map change the cursor back to it's pre-move cursor. If
143     * a double-click occurs center and zoom the map on the clicked location.
144     */
145    @Override
146    public void mouseReleased(MouseEvent e) {
147        if (e.getButton() == MouseEvent.BUTTON1) {
148
149            if (isSelecting && e.getClickCount() == 1) {
150                iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint());
151
152                // reset the selections start and end
153                iEndSelectionPoint = null;
154                iStartSelectionPoint = null;
155                isSelecting = false;
156
157            } else {
158                iSlippyMapChooser.handleAttribution(e.getPoint(), true);
159            }
160        }
161    }
162
163    @Override
164    public void mouseMoved(MouseEvent e) {
165        iSlippyMapChooser.handleAttribution(e.getPoint(), false);
166    }
167
168    private class MoveXAction extends AbstractAction {
169
170        int direction;
171
172        public 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        int direction;
185
186        public 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 = 0;
206
207        /** The vertical direction of movement, -1:up, 0:stop, 1:down */
208        private int directionY = 0;
209
210        /**
211         * Indicated if <code>moveTask</code> is currently enabled (periodically
212         * executed via timer) or disabled
213         */
214        protected boolean scheduled = false;
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            }
266
267            // update the y speed
268            switch (directionY) {
269            case -1:
270                if (speedY > -1) {
271                    speedY = -1;
272                }
273                if (speedY > -1 * MAX_SPEED) {
274                    speedY -= ACCELERATION;
275                }
276                break;
277            case 0:
278                speedY = 0;
279                break;
280            case 1:
281                if (speedY < 1) {
282                    speedY = 1;
283                }
284                if (speedY < MAX_SPEED) {
285                    speedY += ACCELERATION;
286                }
287                break;
288            }
289
290            // move the map
291            int moveX = (int) Math.floor(speedX);
292            int moveY = (int) Math.floor(speedY);
293            if (moveX != 0 || moveY != 0) {
294                iSlippyMapChooser.moveMap(moveX, moveY);
295            }
296        }
297    }
298
299    private class ZoomInAction extends AbstractAction {
300
301        @Override
302        public void actionPerformed(ActionEvent e) {
303            iSlippyMapChooser.zoomIn();
304        }
305    }
306
307    private class ZoomOutAction extends AbstractAction {
308
309        @Override
310        public void actionPerformed(ActionEvent e) {
311            iSlippyMapChooser.zoomOut();
312        }
313    }
314}