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 * @param navComp navigatable component 067 * @param contentPane content pane 068 */ 069 public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane) { 070 iSlippyMapChooser = navComp; 071 iSlippyMapChooser.addMouseListener(this); 072 iSlippyMapChooser.addMouseMotionListener(this); 073 074 if (contentPane != null) { 075 for (int i = 0; i < N.length; ++i) { 076 contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( 077 KeyStroke.getKeyStroke(K[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + N[i]); 078 } 079 } 080 isSelecting = false; 081 082 InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 083 ActionMap actionMap = navComp.getActionMap(); 084 085 // map moving 086 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT"); 087 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT"); 088 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP"); 089 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN"); 090 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY"); 091 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY"); 092 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY"); 093 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY"); 094 095 // zooming. To avoid confusion about which modifier key to use, 096 // we just add all keys left of the space bar 097 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN"); 098 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN"); 099 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN"); 100 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN"); 101 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0, false), "ZOOM_IN"); 102 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0, false), "ZOOM_IN"); 103 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.SHIFT_DOWN_MASK, false), "ZOOM_IN"); 104 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_OUT"); 105 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), "ZOOM_OUT"); 106 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), "ZOOM_OUT"); 107 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0, false), "ZOOM_OUT"); 108 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0, false), "ZOOM_OUT"); 109 110 // action mapping 111 actionMap.put("MOVE_RIGHT", new MoveXAction(1)); 112 actionMap.put("MOVE_LEFT", new MoveXAction(-1)); 113 actionMap.put("MOVE_UP", new MoveYAction(-1)); 114 actionMap.put("MOVE_DOWN", new MoveYAction(1)); 115 actionMap.put("STOP_MOVE_HORIZONTALLY", new MoveXAction(0)); 116 actionMap.put("STOP_MOVE_VERTICALLY", new MoveYAction(0)); 117 actionMap.put("ZOOM_IN", new ZoomInAction()); 118 actionMap.put("ZOOM_OUT", new ZoomOutAction()); 119 } 120 121 /** 122 * Start drawing the selection rectangle if it was the 1st button (left 123 * button) 124 */ 125 @Override 126 public void mousePressed(MouseEvent e) { 127 if (e.getButton() == MouseEvent.BUTTON1 && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { 128 iStartSelectionPoint = e.getPoint(); 129 iEndSelectionPoint = e.getPoint(); 130 } 131 } 132 133 @Override 134 public void mouseDragged(MouseEvent e) { 135 if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK && 136 !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_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 iSlippyMapChooser.handleAttribution(e.getPoint(), true); 163 } 164 } 165 } 166 167 @Override 168 public void mouseMoved(MouseEvent e) { 169 iSlippyMapChooser.handleAttribution(e.getPoint(), false); 170 } 171 172 private class MoveXAction extends AbstractAction { 173 174 private final int direction; 175 176 MoveXAction(int direction) { 177 this.direction = direction; 178 } 179 180 @Override 181 public void actionPerformed(ActionEvent e) { 182 moveTask.setDirectionX(direction); 183 } 184 } 185 186 private class MoveYAction extends AbstractAction { 187 188 private final int direction; 189 190 MoveYAction(int direction) { 191 this.direction = direction; 192 } 193 194 @Override 195 public void actionPerformed(ActionEvent e) { 196 moveTask.setDirectionY(direction); 197 } 198 } 199 200 /** Moves the map depending on which cursor keys are pressed (or not) */ 201 private class MoveTask extends TimerTask { 202 /** The current x speed (pixels per timer interval) */ 203 private double speedX = 1; 204 205 /** The current y speed (pixels per timer interval) */ 206 private double speedY = 1; 207 208 /** The horizontal direction of movement, -1:left, 0:stop, 1:right */ 209 private int directionX; 210 211 /** The vertical direction of movement, -1:up, 0:stop, 1:down */ 212 private int directionY; 213 214 /** 215 * Indicated if <code>moveTask</code> is currently enabled (periodically 216 * executed via timer) or disabled 217 */ 218 protected boolean scheduled; 219 220 protected void setDirectionX(int directionX) { 221 this.directionX = directionX; 222 updateScheduleStatus(); 223 } 224 225 protected void setDirectionY(int directionY) { 226 this.directionY = directionY; 227 updateScheduleStatus(); 228 } 229 230 private void updateScheduleStatus() { 231 boolean newMoveTaskState = !(directionX == 0 && directionY == 0); 232 233 if (newMoveTaskState != scheduled) { 234 scheduled = newMoveTaskState; 235 if (newMoveTaskState) { 236 timer.schedule(this, 0, timerInterval); 237 } else { 238 // We have to create a new instance because rescheduling a 239 // once canceled TimerTask is not possible 240 moveTask = new MoveTask(); 241 cancel(); // Stop this TimerTask 242 } 243 } 244 } 245 246 @Override 247 public void run() { 248 // update the x speed 249 switch (directionX) { 250 case -1: 251 if (speedX > -1) { 252 speedX = -1; 253 } 254 if (speedX > -1 * MAX_SPEED) { 255 speedX -= ACCELERATION; 256 } 257 break; 258 case 0: 259 speedX = 0; 260 break; 261 case 1: 262 if (speedX < 1) { 263 speedX = 1; 264 } 265 if (speedX < MAX_SPEED) { 266 speedX += ACCELERATION; 267 } 268 break; 269 } 270 271 // update the y speed 272 switch (directionY) { 273 case -1: 274 if (speedY > -1) { 275 speedY = -1; 276 } 277 if (speedY > -1 * MAX_SPEED) { 278 speedY -= ACCELERATION; 279 } 280 break; 281 case 0: 282 speedY = 0; 283 break; 284 case 1: 285 if (speedY < 1) { 286 speedY = 1; 287 } 288 if (speedY < MAX_SPEED) { 289 speedY += ACCELERATION; 290 } 291 break; 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}