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 022import org.openstreetmap.josm.Main; 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 */ 032public 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 private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; 050 051 private static final String[] N = { 052 ",", ".", "up", "right", "down", "left"}; 053 private static final int[] K = { 054 KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN, KeyEvent.VK_LEFT}; 055 056 // start and end point of selection rectangle 057 private Point iStartSelectionPoint; 058 private Point iEndSelectionPoint; 059 060 private final SlippyMapBBoxChooser iSlippyMapChooser; 061 062 private boolean isSelecting; 063 064 /** 065 * Constructs a new {@code SlippyMapControler}. 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 121 * button) 122 */ 123 @Override 124 public void mousePressed(MouseEvent e) { 125 if (e.getButton() == MouseEvent.BUTTON1 && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { 126 iStartSelectionPoint = e.getPoint(); 127 iEndSelectionPoint = e.getPoint(); 128 } 129 } 130 131 @Override 132 public void mouseDragged(MouseEvent e) { 133 if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK && 134 !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { 135 if (iStartSelectionPoint != null) { 136 iEndSelectionPoint = e.getPoint(); 137 iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint); 138 isSelecting = true; 139 } 140 } 141 } 142 143 /** 144 * When dragging the map change the cursor back to it's pre-move cursor. If 145 * a double-click occurs center and zoom the map on the clicked location. 146 */ 147 @Override 148 public void mouseReleased(MouseEvent e) { 149 if (e.getButton() == MouseEvent.BUTTON1) { 150 151 if (isSelecting && e.getClickCount() == 1) { 152 iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint()); 153 154 // reset the selections start and end 155 iEndSelectionPoint = null; 156 iStartSelectionPoint = null; 157 isSelecting = false; 158 159 } else { 160 iSlippyMapChooser.handleAttribution(e.getPoint(), true); 161 } 162 } 163 } 164 165 @Override 166 public void mouseMoved(MouseEvent e) { 167 iSlippyMapChooser.handleAttribution(e.getPoint(), false); 168 } 169 170 private class MoveXAction extends AbstractAction { 171 172 private int direction; 173 174 MoveXAction(int direction) { 175 this.direction = direction; 176 } 177 178 @Override 179 public void actionPerformed(ActionEvent e) { 180 moveTask.setDirectionX(direction); 181 } 182 } 183 184 private class MoveYAction extends AbstractAction { 185 186 private int direction; 187 188 MoveYAction(int direction) { 189 this.direction = direction; 190 } 191 192 @Override 193 public void actionPerformed(ActionEvent e) { 194 moveTask.setDirectionY(direction); 195 } 196 } 197 198 /** Moves the map depending on which cursor keys are pressed (or not) */ 199 private class MoveTask extends TimerTask { 200 /** The current x speed (pixels per timer interval) */ 201 private double speedX = 1; 202 203 /** The current y speed (pixels per timer interval) */ 204 private double speedY = 1; 205 206 /** The horizontal direction of movement, -1:left, 0:stop, 1:right */ 207 private int directionX; 208 209 /** The vertical direction of movement, -1:up, 0:stop, 1:down */ 210 private int directionY; 211 212 /** 213 * Indicated if <code>moveTask</code> is currently enabled (periodically 214 * executed via timer) or disabled 215 */ 216 protected boolean scheduled; 217 218 protected void setDirectionX(int directionX) { 219 this.directionX = directionX; 220 updateScheduleStatus(); 221 } 222 223 protected void setDirectionY(int directionY) { 224 this.directionY = directionY; 225 updateScheduleStatus(); 226 } 227 228 private void updateScheduleStatus() { 229 boolean newMoveTaskState = !(directionX == 0 && directionY == 0); 230 231 if (newMoveTaskState != scheduled) { 232 scheduled = newMoveTaskState; 233 if (newMoveTaskState) { 234 timer.schedule(this, 0, timerInterval); 235 } else { 236 // We have to create a new instance because rescheduling a 237 // once canceled TimerTask is not possible 238 moveTask = new MoveTask(); 239 cancel(); // Stop this TimerTask 240 } 241 } 242 } 243 244 @Override 245 public void run() { 246 // update the x speed 247 switch (directionX) { 248 case -1: 249 if (speedX > -1) { 250 speedX = -1; 251 } 252 if (speedX > -1 * MAX_SPEED) { 253 speedX -= ACCELERATION; 254 } 255 break; 256 case 0: 257 speedX = 0; 258 break; 259 case 1: 260 if (speedX < 1) { 261 speedX = 1; 262 } 263 if (speedX < MAX_SPEED) { 264 speedX += ACCELERATION; 265 } 266 break; 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 } 291 292 // move the map 293 int moveX = (int) Math.floor(speedX); 294 int moveY = (int) Math.floor(speedY); 295 if (moveX != 0 || moveY != 0) { 296 iSlippyMapChooser.moveMap(moveX, moveY); 297 } 298 } 299 } 300 301 private class ZoomInAction extends AbstractAction { 302 303 @Override 304 public void actionPerformed(ActionEvent e) { 305 iSlippyMapChooser.zoomIn(); 306 } 307 } 308 309 private class ZoomOutAction extends AbstractAction { 310 311 @Override 312 public void actionPerformed(ActionEvent e) { 313 iSlippyMapChooser.zoomOut(); 314 } 315 } 316}