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}