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.Main; 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 { 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 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 SlippyMapControler}. 064 * @param navComp navigatable component 065 * @param contentPane content pane 066 */ 067 public SlippyMapControler(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 && !(Main.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 ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK && 133 !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK) && iStartSelectionPoint != null) { 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.handleAttribution(e.getPoint(), false); 165 } 166 167 private class MoveXAction extends AbstractAction { 168 169 private final int direction; 170 171 MoveXAction(int direction) { 172 this.direction = direction; 173 } 174 175 @Override 176 public void actionPerformed(ActionEvent e) { 177 moveTask.setDirectionX(direction); 178 } 179 } 180 181 private class MoveYAction extends AbstractAction { 182 183 private final int direction; 184 185 MoveYAction(int direction) { 186 this.direction = direction; 187 } 188 189 @Override 190 public void actionPerformed(ActionEvent e) { 191 moveTask.setDirectionY(direction); 192 } 193 } 194 195 /** Moves the map depending on which cursor keys are pressed (or not) */ 196 private class MoveTask extends TimerTask { 197 /** The current x speed (pixels per timer interval) */ 198 private double speedX = 1; 199 200 /** The current y speed (pixels per timer interval) */ 201 private double speedY = 1; 202 203 /** The horizontal direction of movement, -1:left, 0:stop, 1:right */ 204 private int directionX; 205 206 /** The vertical direction of movement, -1:up, 0:stop, 1:down */ 207 private int directionY; 208 209 /** 210 * Indicated if <code>moveTask</code> is currently enabled (periodically 211 * executed via timer) or disabled 212 */ 213 protected boolean scheduled; 214 215 protected void setDirectionX(int directionX) { 216 this.directionX = directionX; 217 updateScheduleStatus(); 218 } 219 220 protected void setDirectionY(int directionY) { 221 this.directionY = directionY; 222 updateScheduleStatus(); 223 } 224 225 private void updateScheduleStatus() { 226 boolean newMoveTaskState = !(directionX == 0 && directionY == 0); 227 228 if (newMoveTaskState != scheduled) { 229 scheduled = newMoveTaskState; 230 if (newMoveTaskState) { 231 timer.schedule(this, 0, timerInterval); 232 } else { 233 // We have to create a new instance because rescheduling a 234 // once canceled TimerTask is not possible 235 moveTask = new MoveTask(); 236 cancel(); // Stop this TimerTask 237 } 238 } 239 } 240 241 @Override 242 public void run() { 243 // update the x speed 244 switch (directionX) { 245 case -1: 246 if (speedX > -1) { 247 speedX = -1; 248 } 249 if (speedX > -1 * MAX_SPEED) { 250 speedX -= ACCELERATION; 251 } 252 break; 253 case 0: 254 speedX = 0; 255 break; 256 case 1: 257 if (speedX < 1) { 258 speedX = 1; 259 } 260 if (speedX < MAX_SPEED) { 261 speedX += ACCELERATION; 262 } 263 break; 264 default: 265 throw new IllegalStateException(Integer.toString(directionX)); 266 } 267 268 // update the y speed 269 switch (directionY) { 270 case -1: 271 if (speedY > -1) { 272 speedY = -1; 273 } 274 if (speedY > -1 * MAX_SPEED) { 275 speedY -= ACCELERATION; 276 } 277 break; 278 case 0: 279 speedY = 0; 280 break; 281 case 1: 282 if (speedY < 1) { 283 speedY = 1; 284 } 285 if (speedY < MAX_SPEED) { 286 speedY += ACCELERATION; 287 } 288 break; 289 default: 290 throw new IllegalStateException(Integer.toString(directionY)); 291 } 292 293 // move the map 294 int moveX = (int) Math.floor(speedX); 295 int moveY = (int) Math.floor(speedY); 296 if (moveX != 0 || moveY != 0) { 297 iSlippyMapChooser.moveMap(moveX, moveY); 298 } 299 } 300 } 301 302 private class ZoomInAction extends AbstractAction { 303 304 @Override 305 public void actionPerformed(ActionEvent e) { 306 iSlippyMapChooser.zoomIn(); 307 } 308 } 309 310 private class ZoomOutAction extends AbstractAction { 311 312 @Override 313 public void actionPerformed(ActionEvent e) { 314 iSlippyMapChooser.zoomOut(); 315 } 316 } 317}