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 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 implements MouseMotionListener, MouseListener { 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 // start and end point of selection rectangle 048 private Point iStartSelectionPoint; 049 private Point iEndSelectionPoint; 050 051 private final SlippyMapBBoxChooser iSlippyMapChooser; 052 053 private SizeButton iSizeButton = null; 054 private SourceButton iSourceButton = null; 055 056 private boolean isSelecting; 057 058 /** 059 * Constructs a new {@code SlippyMapControler}. 060 */ 061 public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane, SizeButton sizeButton, SourceButton sourceButton) { 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 iSizeButton = sizeButton; 077 iSourceButton = sourceButton; 078 079 isSelecting = false; 080 081 InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 082 ActionMap actionMap = navComp.getActionMap(); 083 084 // map moving 085 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT"); 086 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT"); 087 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP"); 088 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN"); 089 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY"); 090 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY"); 091 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY"); 092 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY"); 093 094 // zooming. To avoid confusion about which modifier key to use, 095 // we just add all keys left of the space bar 096 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN"); 097 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN"); 098 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN"); 099 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN"); 100 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 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) { 126 if (!iSizeButton.hit(e.getPoint())) { 127 iStartSelectionPoint = e.getPoint(); 128 iEndSelectionPoint = e.getPoint(); 129 } 130 } 131 132 } 133 134 @Override 135 public void mouseDragged(MouseEvent e) { 136 if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_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 int sourceButton = iSourceButton.hit(e.getPoint()); 163 164 if (iSizeButton.hit(e.getPoint())) { 165 iSizeButton.toggle(); 166 iSlippyMapChooser.resizeSlippyMap(); 167 } else if (iSlippyMapChooser.handleAttribution(e.getPoint(), true)) { 168 /* do nothing, handleAttribution() already did the work */ 169 } else if (sourceButton == SourceButton.HIDE_OR_SHOW) { 170 iSourceButton.toggle(); 171 iSlippyMapChooser.repaint(); 172 } else if (sourceButton != 0) { 173 iSlippyMapChooser.toggleMapSource(iSourceButton.hitToTileSource(sourceButton)); 174 } 175 } 176 } 177 } 178 179 @Override 180 public void mouseMoved(MouseEvent e) { 181 iSlippyMapChooser.handleAttribution(e.getPoint(), false); 182 } 183 184 private class MoveXAction extends AbstractAction { 185 186 int direction; 187 188 public MoveXAction(int direction) { 189 this.direction = direction; 190 } 191 192 @Override 193 public void actionPerformed(ActionEvent e) { 194 moveTask.setDirectionX(direction); 195 } 196 } 197 198 private class MoveYAction extends AbstractAction { 199 200 int direction; 201 202 public MoveYAction(int direction) { 203 this.direction = direction; 204 } 205 206 @Override 207 public void actionPerformed(ActionEvent e) { 208 moveTask.setDirectionY(direction); 209 } 210 } 211 212 /** Moves the map depending on which cursor keys are pressed (or not) */ 213 private class MoveTask extends TimerTask { 214 /** The current x speed (pixels per timer interval) */ 215 private double speedX = 1; 216 217 /** The current y speed (pixels per timer interval) */ 218 private double speedY = 1; 219 220 /** The horizontal direction of movement, -1:left, 0:stop, 1:right */ 221 private int directionX = 0; 222 223 /** The vertical direction of movement, -1:up, 0:stop, 1:down */ 224 private int directionY = 0; 225 226 /** 227 * Indicated if <code>moveTask</code> is currently enabled (periodically 228 * executed via timer) or disabled 229 */ 230 protected boolean scheduled = false; 231 232 protected void setDirectionX(int directionX) { 233 this.directionX = directionX; 234 updateScheduleStatus(); 235 } 236 237 protected void setDirectionY(int directionY) { 238 this.directionY = directionY; 239 updateScheduleStatus(); 240 } 241 242 private void updateScheduleStatus() { 243 boolean newMoveTaskState = !(directionX == 0 && directionY == 0); 244 245 if (newMoveTaskState != scheduled) { 246 scheduled = newMoveTaskState; 247 if (newMoveTaskState) { 248 timer.schedule(this, 0, timerInterval); 249 } else { 250 // We have to create a new instance because rescheduling a 251 // once canceled TimerTask is not possible 252 moveTask = new MoveTask(); 253 cancel(); // Stop this TimerTask 254 } 255 } 256 } 257 258 @Override 259 public void run() { 260 // update the x speed 261 switch (directionX) { 262 case -1: 263 if (speedX > -1) { 264 speedX = -1; 265 } 266 if (speedX > -1 * MAX_SPEED) { 267 speedX -= ACCELERATION; 268 } 269 break; 270 case 0: 271 speedX = 0; 272 break; 273 case 1: 274 if (speedX < 1) { 275 speedX = 1; 276 } 277 if (speedX < MAX_SPEED) { 278 speedX += ACCELERATION; 279 } 280 break; 281 } 282 283 // update the y speed 284 switch (directionY) { 285 case -1: 286 if (speedY > -1) { 287 speedY = -1; 288 } 289 if (speedY > -1 * MAX_SPEED) { 290 speedY -= ACCELERATION; 291 } 292 break; 293 case 0: 294 speedY = 0; 295 break; 296 case 1: 297 if (speedY < 1) { 298 speedY = 1; 299 } 300 if (speedY < MAX_SPEED) { 301 speedY += ACCELERATION; 302 } 303 break; 304 } 305 306 // move the map 307 int moveX = (int) Math.floor(speedX); 308 int moveY = (int) Math.floor(speedY); 309 if (moveX != 0 || moveY != 0) { 310 iSlippyMapChooser.moveMap(moveX, moveY); 311 } 312 } 313 } 314 315 private class ZoomInAction extends AbstractAction { 316 317 @Override 318 public void actionPerformed(ActionEvent e) { 319 iSlippyMapChooser.zoomIn(); 320 } 321 } 322 323 private class ZoomOutAction extends AbstractAction { 324 325 @Override 326 public void actionPerformed(ActionEvent e) { 327 iSlippyMapChooser.zoomOut(); 328 } 329 } 330}