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.util.Timer; 011import java.util.TimerTask; 012 013import javax.swing.AbstractAction; 014import javax.swing.ActionMap; 015import javax.swing.InputMap; 016import javax.swing.JComponent; 017import javax.swing.JPanel; 018import javax.swing.KeyStroke; 019 020import org.openstreetmap.josm.tools.PlatformManager; 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 SlippyMapController extends MouseAdapter { 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 final 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 private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; 048 049 private static final String[] N = { 050 ",", ".", "up", "right", "down", "left"}; 051 private static final int[] K = { 052 KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN, KeyEvent.VK_LEFT}; 053 054 // start and end point of selection rectangle 055 private Point iStartSelectionPoint; 056 private Point iEndSelectionPoint; 057 058 private final SlippyMapBBoxChooser iSlippyMapChooser; 059 060 private boolean isSelecting; 061 062 /** 063 * Constructs a new {@code SlippyMapController}. 064 * @param navComp navigable component 065 * @param contentPane content pane 066 */ 067 public SlippyMapController(SlippyMapBBoxChooser navComp, JPanel contentPane) { 068 iSlippyMapChooser = navComp; 069 iSlippyMapChooser.addMouseListener(this); 070 iSlippyMapChooser.addMouseMotionListener(this); 071 072 if (contentPane != null) { 073 for (int i = 0; i < N.length; ++i) { 074 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 075 KeyStroke.getKeyStroke(K[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + N[i]); 076 } 077 } 078 isSelecting = false; 079 080 InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 081 ActionMap actionMap = navComp.getActionMap(); 082 083 // map moving 084 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT"); 085 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT"); 086 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP"); 087 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN"); 088 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY"); 089 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY"); 090 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY"); 091 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY"); 092 093 // zooming. To avoid confusion about which modifier key to use, 094 // we just add all keys left of the space bar 095 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN"); 096 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN"); 097 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN"); 098 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN"); 099 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0, false), "ZOOM_IN"); 100 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 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 button) 121 */ 122 @Override 123 public void mousePressed(MouseEvent e) { 124 if (e.getButton() == MouseEvent.BUTTON1 && !(PlatformManager.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { 125 iStartSelectionPoint = e.getPoint(); 126 iEndSelectionPoint = e.getPoint(); 127 } 128 } 129 130 @Override 131 public void mouseDragged(MouseEvent e) { 132 if (iStartSelectionPoint != null && (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK 133 && !(PlatformManager.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { 134 iEndSelectionPoint = e.getPoint(); 135 iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint); 136 isSelecting = true; 137 } 138 } 139 140 /** 141 * When dragging the map change the cursor back to it's pre-move cursor. If 142 * a double-click occurs center and zoom the map on the clicked location. 143 */ 144 @Override 145 public void mouseReleased(MouseEvent e) { 146 if (e.getButton() == MouseEvent.BUTTON1) { 147 148 if (isSelecting && e.getClickCount() == 1) { 149 iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint()); 150 151 // reset the selections start and end 152 iEndSelectionPoint = null; 153 iStartSelectionPoint = null; 154 isSelecting = false; 155 156 } else { 157 iSlippyMapChooser.handleAttribution(e.getPoint(), true); 158 } 159 } 160 } 161 162 @Override 163 public void mouseMoved(MouseEvent e) { 164 iSlippyMapChooser.handleMouseMoved(e.getPoint()); 165 iSlippyMapChooser.handleAttribution(e.getPoint(), false); 166 } 167 168 private class MoveXAction extends AbstractAction { 169 170 private final int direction; 171 172 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 private final int direction; 185 186 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; 206 207 /** The vertical direction of movement, -1:up, 0:stop, 1:down */ 208 private int directionY; 209 210 /** 211 * Indicated if <code>moveTask</code> is currently enabled (periodically 212 * executed via timer) or disabled 213 */ 214 protected boolean scheduled; 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 default: 266 throw new IllegalStateException(Integer.toString(directionX)); 267 } 268 269 // update the y speed 270 switch (directionY) { 271 case -1: 272 if (speedY > -1) { 273 speedY = -1; 274 } 275 if (speedY > -1 * MAX_SPEED) { 276 speedY -= ACCELERATION; 277 } 278 break; 279 case 0: 280 speedY = 0; 281 break; 282 case 1: 283 if (speedY < 1) { 284 speedY = 1; 285 } 286 if (speedY < MAX_SPEED) { 287 speedY += ACCELERATION; 288 } 289 break; 290 default: 291 throw new IllegalStateException(Integer.toString(directionY)); 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}