// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.bbox; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Timer; import java.util.TimerTask; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.KeyStroke; import org.openstreetmap.josm.Main; /** * This class controls the user input by listening to mouse and key events. * Currently implemented is: - zooming in and out with scrollwheel - zooming in * and centering by double clicking - selecting an area by clicking and dragging * the mouse * * @author Tim Haussmann */ public class SlippyMapControler extends MouseAdapter { /** A Timer for smoothly moving the map area */ private static final Timer timer = new Timer(true); /** Does the moving */ private MoveTask moveTask = new MoveTask(); /** How often to do the moving (milliseconds) */ private static long timerInterval = 20; /** The maximum speed (pixels per timer interval) */ private static final double MAX_SPEED = 20; /** The speed increase per timer interval when a cursor button is clicked */ private static final double ACCELERATION = 0.10; private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; private static final String[] N = { ",", ".", "up", "right", "down", "left"}; private static final int[] K = { KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_UP, KeyEvent.VK_RIGHT, KeyEvent.VK_DOWN, KeyEvent.VK_LEFT}; // start and end point of selection rectangle private Point iStartSelectionPoint; private Point iEndSelectionPoint; private final SlippyMapBBoxChooser iSlippyMapChooser; private boolean isSelecting; /** * Constructs a new {@code SlippyMapControler}. * @param navComp navigatable component * @param contentPane content pane */ public SlippyMapControler(SlippyMapBBoxChooser navComp, JPanel contentPane) { iSlippyMapChooser = navComp; iSlippyMapChooser.addMouseListener(this); iSlippyMapChooser.addMouseMotionListener(this); if (contentPane != null) { for (int i = 0; i < N.length; ++i) { contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( KeyStroke.getKeyStroke(K[i], KeyEvent.CTRL_DOWN_MASK), "MapMover.Zoomer." + N[i]); } } isSelecting = false; InputMap inputMap = navComp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap = navComp.getActionMap(); // map moving inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY"); // zooming. To avoid confusion about which modifier key to use, // we just add all keys left of the space bar inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.SHIFT_DOWN_MASK, false), "ZOOM_IN"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), "ZOOM_OUT"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), "ZOOM_OUT"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), "ZOOM_OUT"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0, false), "ZOOM_OUT"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0, false), "ZOOM_OUT"); // action mapping actionMap.put("MOVE_RIGHT", new MoveXAction(1)); actionMap.put("MOVE_LEFT", new MoveXAction(-1)); actionMap.put("MOVE_UP", new MoveYAction(-1)); actionMap.put("MOVE_DOWN", new MoveYAction(1)); actionMap.put("STOP_MOVE_HORIZONTALLY", new MoveXAction(0)); actionMap.put("STOP_MOVE_VERTICALLY", new MoveYAction(0)); actionMap.put("ZOOM_IN", new ZoomInAction()); actionMap.put("ZOOM_OUT", new ZoomOutAction()); } /** * Start drawing the selection rectangle if it was the 1st button (left button) */ @Override public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1 && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { iStartSelectionPoint = e.getPoint(); iEndSelectionPoint = e.getPoint(); } } @Override public void mouseDragged(MouseEvent e) { if (iStartSelectionPoint != null && (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK && !(Main.isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) { iEndSelectionPoint = e.getPoint(); iSlippyMapChooser.setSelection(iStartSelectionPoint, iEndSelectionPoint); isSelecting = true; } } /** * When dragging the map change the cursor back to it's pre-move cursor. If * a double-click occurs center and zoom the map on the clicked location. */ @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { if (isSelecting && e.getClickCount() == 1) { iSlippyMapChooser.setSelection(iStartSelectionPoint, e.getPoint()); // reset the selections start and end iEndSelectionPoint = null; iStartSelectionPoint = null; isSelecting = false; } else { iSlippyMapChooser.handleAttribution(e.getPoint(), true); } } } @Override public void mouseMoved(MouseEvent e) { iSlippyMapChooser.handleAttribution(e.getPoint(), false); } private class MoveXAction extends AbstractAction { private final int direction; MoveXAction(int direction) { this.direction = direction; } @Override public void actionPerformed(ActionEvent e) { moveTask.setDirectionX(direction); } } private class MoveYAction extends AbstractAction { private final int direction; MoveYAction(int direction) { this.direction = direction; } @Override public void actionPerformed(ActionEvent e) { moveTask.setDirectionY(direction); } } /** Moves the map depending on which cursor keys are pressed (or not) */ private class MoveTask extends TimerTask { /** The current x speed (pixels per timer interval) */ private double speedX = 1; /** The current y speed (pixels per timer interval) */ private double speedY = 1; /** The horizontal direction of movement, -1:left, 0:stop, 1:right */ private int directionX; /** The vertical direction of movement, -1:up, 0:stop, 1:down */ private int directionY; /** * Indicated if <code>moveTask</code> is currently enabled (periodically * executed via timer) or disabled */ protected boolean scheduled; protected void setDirectionX(int directionX) { this.directionX = directionX; updateScheduleStatus(); } protected void setDirectionY(int directionY) { this.directionY = directionY; updateScheduleStatus(); } private void updateScheduleStatus() { boolean newMoveTaskState = !(directionX == 0 && directionY == 0); if (newMoveTaskState != scheduled) { scheduled = newMoveTaskState; if (newMoveTaskState) { timer.schedule(this, 0, timerInterval); } else { // We have to create a new instance because rescheduling a // once canceled TimerTask is not possible moveTask = new MoveTask(); cancel(); // Stop this TimerTask } } } @Override public void run() { // update the x speed switch (directionX) { case -1: if (speedX > -1) { speedX = -1; } if (speedX > -1 * MAX_SPEED) { speedX -= ACCELERATION; } break; case 0: speedX = 0; break; case 1: if (speedX < 1) { speedX = 1; } if (speedX < MAX_SPEED) { speedX += ACCELERATION; } break; default: throw new IllegalStateException(Integer.toString(directionX)); } // update the y speed switch (directionY) { case -1: if (speedY > -1) { speedY = -1; } if (speedY > -1 * MAX_SPEED) { speedY -= ACCELERATION; } break; case 0: speedY = 0; break; case 1: if (speedY < 1) { speedY = 1; } if (speedY < MAX_SPEED) { speedY += ACCELERATION; } break; default: throw new IllegalStateException(Integer.toString(directionY)); } // move the map int moveX = (int) Math.floor(speedX); int moveY = (int) Math.floor(speedY); if (moveX != 0 || moveY != 0) { iSlippyMapChooser.moveMap(moveX, moveY); } } } private class ZoomInAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { iSlippyMapChooser.zoomIn(); } } private class ZoomOutAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { iSlippyMapChooser.zoomOut(); } } }