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
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 implements MouseMotionListener, MouseListener {
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    // start and end point of selection rectangle
048    private Point iStartSelectionPoint;
049    private Point iEndSelectionPoint;
050
051    private final SlippyMapBBoxChooser iSlippyMapChooser;
052
053    private SizeButton iSizeButton = null;
054    private SourceButton iSourceButton = null;
055
056    private boolean isSelecting;
057
058    /**
059     * Constructs a new {@code SlippyMapControler}.
060     */
061    public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane, SizeButton sizeButton, SourceButton sourceButton) {
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        iSizeButton = sizeButton;
077        iSourceButton = sourceButton;
078
079        isSelecting = false;
080
081        InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
082        ActionMap actionMap = navComp.getActionMap();
083
084        // map moving
085        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT");
086        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT");
087        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP");
088        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN");
089        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY");
090        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY");
091        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY");
092        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY");
093
094        // zooming. To avoid confusion about which modifier key to use,
095        // we just add all keys left of the space bar
096        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN");
097        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN");
098        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN");
099        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN");
100        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 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) {
126            if (!iSizeButton.hit(e.getPoint())) {
127                iStartSelectionPoint = e.getPoint();
128                iEndSelectionPoint = e.getPoint();
129            }
130        }
131
132    }
133
134    @Override
135    public void mouseDragged(MouseEvent e) {
136        if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_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                int sourceButton = iSourceButton.hit(e.getPoint());
163
164                if (iSizeButton.hit(e.getPoint())) {
165                    iSizeButton.toggle();
166                    iSlippyMapChooser.resizeSlippyMap();
167                } else if (iSlippyMapChooser.handleAttribution(e.getPoint(), true)) {
168                    /* do nothing, handleAttribution() already did the work */
169                } else if (sourceButton == SourceButton.HIDE_OR_SHOW) {
170                    iSourceButton.toggle();
171                    iSlippyMapChooser.repaint();
172                } else if (sourceButton != 0) {
173                    iSlippyMapChooser.toggleMapSource(iSourceButton.hitToTileSource(sourceButton));
174                }
175            }
176        }
177    }
178
179    @Override
180    public void mouseMoved(MouseEvent e) {
181        iSlippyMapChooser.handleAttribution(e.getPoint(), false);
182    }
183
184    private class MoveXAction extends AbstractAction {
185
186        int direction;
187
188        public MoveXAction(int direction) {
189            this.direction = direction;
190        }
191
192        @Override
193        public void actionPerformed(ActionEvent e) {
194            moveTask.setDirectionX(direction);
195        }
196    }
197
198    private class MoveYAction extends AbstractAction {
199
200        int direction;
201
202        public MoveYAction(int direction) {
203            this.direction = direction;
204        }
205
206        @Override
207        public void actionPerformed(ActionEvent e) {
208            moveTask.setDirectionY(direction);
209        }
210    }
211
212    /** Moves the map depending on which cursor keys are pressed (or not) */
213    private class MoveTask extends TimerTask {
214        /** The current x speed (pixels per timer interval) */
215        private double speedX = 1;
216
217        /** The current y speed (pixels per timer interval) */
218        private double speedY = 1;
219
220        /** The horizontal direction of movement, -1:left, 0:stop, 1:right */
221        private int directionX = 0;
222
223        /** The vertical direction of movement, -1:up, 0:stop, 1:down */
224        private int directionY = 0;
225
226        /**
227         * Indicated if <code>moveTask</code> is currently enabled (periodically
228         * executed via timer) or disabled
229         */
230        protected boolean scheduled = false;
231
232        protected void setDirectionX(int directionX) {
233            this.directionX = directionX;
234            updateScheduleStatus();
235        }
236
237        protected void setDirectionY(int directionY) {
238            this.directionY = directionY;
239            updateScheduleStatus();
240        }
241
242        private void updateScheduleStatus() {
243            boolean newMoveTaskState = !(directionX == 0 && directionY == 0);
244
245            if (newMoveTaskState != scheduled) {
246                scheduled = newMoveTaskState;
247                if (newMoveTaskState) {
248                    timer.schedule(this, 0, timerInterval);
249                } else {
250                    // We have to create a new instance because rescheduling a
251                    // once canceled TimerTask is not possible
252                    moveTask = new MoveTask();
253                    cancel(); // Stop this TimerTask
254                }
255            }
256        }
257
258        @Override
259        public void run() {
260            // update the x speed
261            switch (directionX) {
262            case -1:
263                if (speedX > -1) {
264                    speedX = -1;
265                }
266                if (speedX > -1 * MAX_SPEED) {
267                    speedX -= ACCELERATION;
268                }
269                break;
270            case 0:
271                speedX = 0;
272                break;
273            case 1:
274                if (speedX < 1) {
275                    speedX = 1;
276                }
277                if (speedX < MAX_SPEED) {
278                    speedX += ACCELERATION;
279                }
280                break;
281            }
282
283            // update the y speed
284            switch (directionY) {
285            case -1:
286                if (speedY > -1) {
287                    speedY = -1;
288                }
289                if (speedY > -1 * MAX_SPEED) {
290                    speedY -= ACCELERATION;
291                }
292                break;
293            case 0:
294                speedY = 0;
295                break;
296            case 1:
297                if (speedY < 1) {
298                    speedY = 1;
299                }
300                if (speedY < MAX_SPEED) {
301                    speedY += ACCELERATION;
302                }
303                break;
304            }
305
306            // move the map
307            int moveX = (int) Math.floor(speedX);
308            int moveY = (int) Math.floor(speedY);
309            if (moveX != 0 || moveY != 0) {
310                iSlippyMapChooser.moveMap(moveX, moveY);
311            }
312        }
313    }
314
315    private class ZoomInAction extends AbstractAction {
316
317        @Override
318        public void actionPerformed(ActionEvent e) {
319            iSlippyMapChooser.zoomIn();
320        }
321    }
322
323    private class ZoomOutAction extends AbstractAction {
324
325        @Override
326        public void actionPerformed(ActionEvent e) {
327            iSlippyMapChooser.zoomOut();
328        }
329    }
330}