/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.swing.event;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JComponent;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.swing.MapPane;
/**
* Handles keyboard events for a map pane. This is the default handler for classes derived from
* {@linkplain AbstractMapPane}. It provides for keyboard-controlled scrolling and zooming of
* the display. The default key bindings for actions should be suitable for most keyboards.
* <p>
*
* While the Java Swing toolkit provides its own mechanism for linking key events to actions,
* this class is somewhat easier to use and provides a model that could be implemented in other
* toolkits such as SWT. However, you are free to ignore this class and use your own key
* handler instead since the map pane classes only require that the handler implements
* the {@linkplain java.awt.event.KeyListener} interface.
*
* <p>
* Key bindings for an individual action can be set like this:
* <pre><code>
* // Bind left-scroll action to the 'h' key (for Vim fans)
* KeyInfo key = new KeyInfo(KeyEvent.VK_H, 0);
* mapPaneKeyHandler.setBinding(key, MapPaneKeyHandler.Action.SCROLL_LEFT);
* </code></pre>
*
* Multiple bindings can be set with the {@linkplain #setBindings(Map)} or
* {@linkplain #setAllBindings(Map)} methods:
* <pre><code>
* Map<KeyInfo, MapPaneKeyHandler.Action> bindings =
* new HashMap<KeyInfo, MapPaneKeyHandler.Action>();
*
* bindings.put(new KeyInfo(KeyEvent.VK_H, 0), MapPaneKeyHandler.Action.SCROLL_LEFT);
* bindings.put(new KeyInfo(KeyEvent.VK_L, 0), MapPaneKeyHandler.Action.SCROLL_RIGHT);
* bindings.put(new KeyInfo(KeyEvent.VK_K, 0), MapPaneKeyHandler.Action.SCROLL_UP);
* bindings.put(new KeyInfo(KeyEvent.VK_J, 0), MapPaneKeyHandler.Action.SCROLL_DOWN);
*
* mapPaneKeyHandler.setBindings( bindings );
* </code></pre>
*
* @see KeyInfo
* @see AbstractMapPane#setKeyHandler(java.awt.event.KeyListener)
*
* @author Michael Bedward
* @since 8.0
*
* @source $URL$
* @version $Id$
*/
public class MapPaneKeyHandler extends KeyAdapter {
private static final double SCROLL_FRACTION = 0.05;
private static final double ZOOM_FRACTION = 1.5;
/**
* Constants for supported actions.
*/
public static enum Action {
SCROLL_LEFT,
SCROLL_RIGHT,
SCROLL_UP,
SCROLL_DOWN,
ZOOM_IN,
ZOOM_OUT,
ZOOM_FULL_EXTENT;
}
/*
* Default key bindings
*/
private static final Map<KeyInfo, Action> defaultBindings = new HashMap<KeyInfo, Action>();
static {
defaultBindings.put(
new KeyInfo(KeyEvent.VK_LEFT, 0, "Left"),
Action.SCROLL_LEFT);
defaultBindings.put(
new KeyInfo(KeyEvent.VK_RIGHT, 0, "Right"),
Action.SCROLL_RIGHT);
defaultBindings.put(
new KeyInfo(KeyEvent.VK_UP, 0, "Up"),
Action.SCROLL_UP);
defaultBindings.put(
new KeyInfo(KeyEvent.VK_DOWN, 0, "Down"),
Action.SCROLL_DOWN);
defaultBindings.put(
new KeyInfo(KeyEvent.VK_UP, KeyEvent.SHIFT_DOWN_MASK, "Shift+Up"),
Action.ZOOM_IN);
defaultBindings.put(
new KeyInfo(KeyEvent.VK_DOWN, KeyEvent.SHIFT_DOWN_MASK, "Shift+Down"),
Action.ZOOM_OUT);
defaultBindings.put(
new KeyInfo(KeyEvent.VK_EQUALS, 0, "="),
Action.ZOOM_FULL_EXTENT);
}
private final Map<KeyInfo, Action> bindings;
private final MapPane mapPane;
/**
* Creates a new instance with the default key bindings for actions.
*
* @param mapPane the map pane associated with this handler
*/
public MapPaneKeyHandler(MapPane mapPane) {
this.bindings = new HashMap<KeyInfo, Action>(defaultBindings);
this.mapPane = mapPane;
}
/**
* Sets all key bindings to their default value.
*/
public void setDefaultBindings() {
bindings.clear();
bindings.putAll(defaultBindings);
}
/**
* Gets the current key bindings. The bindings are copied into the
* destination {@code Map}, so subsequent changes to it will not affect
* this handler.
*
* @return the current key bindings
*/
public Map<KeyInfo, Action> getBindings() {
Map<KeyInfo, Action> map = new HashMap<KeyInfo, Action>();
for (Map.Entry<KeyInfo, Action> e : bindings.entrySet()) {
map.put(new KeyInfo(e.getKey()), e.getValue());
}
return map;
}
/**
* Gets the current key binding for the given action. The object
* returned is a copy.
*
* @param action the action
* @return the key binding; or {@code null} if there is no binding
* @throws IllegalArgumentException if {@code action} is {@code null}
*/
public KeyInfo getBindingForAction(Action action) {
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
KeyInfo keyInfo = null;
for (Map.Entry<KeyInfo, Action> e : bindings.entrySet()) {
if (e.getValue() == action) {
keyInfo = new KeyInfo(e.getKey());
break;
}
}
return keyInfo;
}
/**
* Sets the key binding for a single action.
*
* @param keyInfo the key binding
* @param action the action
* @throws IllegalArgumentException if either argument is {@code null}
*/
public void setBinding(KeyInfo keyInfo, Action action) {
if (keyInfo == null) {
throw new IllegalArgumentException("keyInfo must not be null");
}
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
bindings.put(new KeyInfo(keyInfo), action);
}
/**
* Sets one or more key bindings for actions. This method can be used to
* set a subset of the key bindings while leaving others unchanged.
*
* @param newBindings new key bindings
* @throws IllegalArgumentException if {@code newBindings} is {@code null}
*/
public void setBindings(Map<KeyInfo, Action> newBindings) {
if (newBindings == null) {
throw new IllegalArgumentException("argument must not be null");
}
for (Map.Entry<KeyInfo, Action> e : newBindings.entrySet()) {
setBinding(e.getKey(), e.getValue());
}
}
/**
* Sets the bindings to those specified in {@code newBindings}. This method
* differs to {@linkplain #setBindings(java.util.Map)} in that any actions
* which do not appear in the input map are disabled.
*
* @param newBindings new key bindings
* @throws IllegalArgumentException if {@code newBindings} is {@code null}
*/
public void setAllBindings(Map<KeyInfo, Action> newBindings) {
if (newBindings == null) {
throw new IllegalArgumentException("argument must not be null");
}
bindings.clear();
setBindings(newBindings);
}
/**
* Handles a key-pressed event.
*
* @param e input key event
*/
@Override
public void keyPressed(KeyEvent e) {
for (KeyInfo keyInfo : bindings.keySet()) {
if (keyInfo.matchesEvent(e)) {
processAction(bindings.get(keyInfo));
}
}
}
/**
* Directs a requested action to the corresponding method.
*
* @param action the action
*/
private void processAction(Action action) {
switch (action) {
case SCROLL_LEFT:
case SCROLL_RIGHT:
case SCROLL_UP:
case SCROLL_DOWN:
scroll(action);
break;
case ZOOM_IN:
case ZOOM_OUT:
case ZOOM_FULL_EXTENT:
zoom(action);
break;
}
}
/**
* Scrolls the map pane image. We use {@linkplain MapPane#moveImage(int, int)}
* rather than {@linkplain MapPane#setDisplayArea(<any>)} in this method
* because it gives much smoother scrolling when the key is held down.
*
* @param action scroll direction
*/
private void scroll(Action action) {
Rectangle r = ((JComponent) mapPane).getVisibleRect();
if (!(r == null || r.isEmpty())) {
int dx = 0;
int dy = 0;
switch (action) {
case SCROLL_LEFT:
dx = Math.max(1, (int) (r.getWidth() * SCROLL_FRACTION));
break;
case SCROLL_RIGHT:
dx = Math.min(-1, (int) (-r.getWidth() * SCROLL_FRACTION));
break;
case SCROLL_UP:
dy = Math.max(1, (int) (r.getWidth() * SCROLL_FRACTION));
break;
case SCROLL_DOWN:
dy = Math.min(-1, (int) (-r.getWidth() * SCROLL_FRACTION));
break;
default:
throw new IllegalArgumentException("Invalid action argument: " + action);
}
mapPane.moveImage(dx, dy);
}
}
/**
* Zooms the map pane image.
*
* @param action zoom action
*/
private void zoom(Action action) {
ReferencedEnvelope env = mapPane.getDisplayArea();
double zoom;
if (!env.isEmpty()) {
switch (action) {
case ZOOM_FULL_EXTENT:
mapPane.reset();
return;
case ZOOM_IN:
zoom = 1.0 / ZOOM_FRACTION;
break;
case ZOOM_OUT:
zoom = ZOOM_FRACTION;
break;
default:
throw new IllegalArgumentException("invalid action argument: " + action);
}
double centreX = env.getMedian(0);
double centreY = env.getMedian(1);
double w = env.getWidth() * zoom;
double h = env.getHeight() * zoom;
ReferencedEnvelope newEnv = new ReferencedEnvelope(
centreX - w / 2,
centreX + w / 2,
centreY - h / 2,
centreY + h / 2,
env.getCoordinateReferenceSystem());
mapPane.setDisplayArea(newEnv);
}
}
}