001    // This code has been adapted and copied from code that has been written by Immanuel Scholz and others for JOSM.
002    // License: GPL. Copyright 2007 by Tim Haussmann
003    package org.openstreetmap.josm.gui.bbox;
004    
005    import java.awt.Point;
006    import java.awt.event.ActionEvent;
007    import java.awt.event.InputEvent;
008    import java.awt.event.KeyEvent;
009    import java.awt.event.MouseAdapter;
010    import java.awt.event.MouseEvent;
011    import java.awt.event.MouseListener;
012    import java.awt.event.MouseMotionListener;
013    import java.util.Timer;
014    import java.util.TimerTask;
015    
016    import javax.swing.AbstractAction;
017    import javax.swing.ActionMap;
018    import javax.swing.InputMap;
019    import javax.swing.JComponent;
020    import javax.swing.JPanel;
021    import javax.swing.KeyStroke;
022    
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     */
032    public 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        // start and end point of selection rectangle
050        private Point iStartSelectionPoint;
051        private Point iEndSelectionPoint;
052    
053        // the SlippyMapChooserComponent
054        private final SlippyMapBBoxChooser iSlippyMapChooser;
055    
056        private SizeButton iSizeButton = null;
057        private SourceButton iSourceButton = null;
058        
059        private boolean isSelecting;
060    
061        /**
062         * Create a new OsmMapControl
063         */
064        public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane, SizeButton sizeButton, SourceButton sourceButton) {
065            this.iSlippyMapChooser = navComp;
066            iSlippyMapChooser.addMouseListener(this);
067            iSlippyMapChooser.addMouseMotionListener(this);
068    
069            String[] n = { ",", ".", "up", "right", "down", "left" };
070            int[] k = { KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN,
071                    KeyEvent.VK_LEFT };
072    
073            if (contentPane != null) {
074                for (int i = 0; i < n.length; ++i) {
075                    contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
076                            KeyStroke.getKeyStroke(k[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + n[i]);
077                }
078            }
079            iSizeButton = sizeButton;
080            iSourceButton = sourceButton;
081            
082            isSelecting = false;
083    
084            InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
085            ActionMap actionMap = navComp.getActionMap();
086    
087            // map moving
088            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT");
089            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT");
090            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP");
091            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN");
092            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY");
093            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY");
094            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY");
095            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY");
096    
097            // zooming. To avoid confusion about which modifier key to use,
098            // we just add all keys left of the space bar
099            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN");
100            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN");
101            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN");
102            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN");
103            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_OUT");
104            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), "ZOOM_OUT");
105            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), "ZOOM_OUT");
106            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 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            public void actionPerformed(ActionEvent e) {
193                moveTask.setDirectionX(direction);
194            }
195        }
196    
197        private class MoveYAction extends AbstractAction {
198    
199            int direction;
200    
201            public MoveYAction(int direction) {
202                this.direction = direction;
203            }
204    
205            public void actionPerformed(ActionEvent e) {
206                moveTask.setDirectionY(direction);
207            }
208        }
209    
210        /** Moves the map depending on which cursor keys are pressed (or not) */
211        private class MoveTask extends TimerTask {
212            /** The current x speed (pixels per timer interval) */
213            private double speedX = 1;
214    
215            /** The current y speed (pixels per timer interval) */
216            private double speedY = 1;
217    
218            /** The horizontal direction of movement, -1:left, 0:stop, 1:right */
219            private int directionX = 0;
220    
221            /** The vertical direction of movement, -1:up, 0:stop, 1:down */
222            private int directionY = 0;
223    
224            /**
225             * Indicated if <code>moveTask</code> is currently enabled (periodically
226             * executed via timer) or disabled
227             */
228            protected boolean scheduled = false;
229    
230            protected void setDirectionX(int directionX) {
231                this.directionX = directionX;
232                updateScheduleStatus();
233            }
234    
235            protected void setDirectionY(int directionY) {
236                this.directionY = directionY;
237                updateScheduleStatus();
238            }
239    
240            private void updateScheduleStatus() {
241                boolean newMoveTaskState = !(directionX == 0 && directionY == 0);
242    
243                if (newMoveTaskState != scheduled) {
244                    scheduled = newMoveTaskState;
245                    if (newMoveTaskState) {
246                        timer.schedule(this, 0, timerInterval);
247                    } else {
248                        // We have to create a new instance because rescheduling a
249                        // once canceled TimerTask is not possible
250                        moveTask = new MoveTask();
251                        cancel(); // Stop this TimerTask
252                    }
253                }
254            }
255    
256            @Override
257            public void run() {
258                // update the x speed
259                switch (directionX) {
260                case -1:
261                    if (speedX > -1) {
262                        speedX = -1;
263                    }
264                    if (speedX > -1 * MAX_SPEED) {
265                        speedX -= ACCELERATION;
266                    }
267                    break;
268                case 0:
269                    speedX = 0;
270                    break;
271                case 1:
272                    if (speedX < 1) {
273                        speedX = 1;
274                    }
275                    if (speedX < MAX_SPEED) {
276                        speedX += ACCELERATION;
277                    }
278                    break;
279                }
280    
281                // update the y speed
282                switch (directionY) {
283                case -1:
284                    if (speedY > -1) {
285                        speedY = -1;
286                    }
287                    if (speedY > -1 * MAX_SPEED) {
288                        speedY -= ACCELERATION;
289                    }
290                    break;
291                case 0:
292                    speedY = 0;
293                    break;
294                case 1:
295                    if (speedY < 1) {
296                        speedY = 1;
297                    }
298                    if (speedY < MAX_SPEED) {
299                        speedY += ACCELERATION;
300                    }
301                    break;
302                }
303    
304                // move the map
305                int moveX = (int) Math.floor(speedX);
306                int moveY = (int) Math.floor(speedY);
307                if (moveX != 0 || moveY != 0) {
308                    iSlippyMapChooser.moveMap(moveX, moveY);
309                }
310            }
311        }
312    
313        private class ZoomInAction extends AbstractAction {
314    
315            public void actionPerformed(ActionEvent e) {
316                iSlippyMapChooser.zoomIn();
317            }
318        }
319    
320        private class ZoomOutAction extends AbstractAction {
321    
322            public void actionPerformed(ActionEvent e) {
323                iSlippyMapChooser.zoomOut();
324            }
325        }
326    }