// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.actions;
import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.data.coor.CoordinateFormat;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.WindowGeometry;
/**
* This action displays a dialog where the user can enter a latitude and longitude,
* and when ok is pressed, a new node is created at the specified position.
*/
public final class AddNodeAction extends JosmAction {
//static private final Logger logger = Logger.getLogger(AddNodeAction.class.getName());
public AddNodeAction() {
super(tr("Add Node..."), "addnode", tr("Add a node by entering latitude and longitude."),
Shortcut.registerShortcut("addnode", tr("Edit: {0}", tr("Add Node...")), KeyEvent.VK_D, Shortcut.GROUP_EDIT,
Shortcut.SHIFT_DEFAULT), true);
putValue("help", ht("/Action/AddNode"));
}
public void actionPerformed(ActionEvent e) {
if (!isEnabled())
return;
LatLonDialog dialog = new LatLonDialog(Main.parent);
dialog.setVisible(true);
if (dialog.isCanceled())
return;
LatLon coordinates = dialog.getCoordinates();
if (coordinates == null)
return;
Node nnew = new Node(coordinates);
// add the node
Main.main.undoRedo.add(new AddCommand(nnew));
getCurrentDataSet().setSelected(nnew);
Main.map.mapView.repaint();
}
@Override
protected void updateEnabledState() {
setEnabled(getEditLayer() != null);
}
static private class LatLonDialog extends JDialog {
private static final Color BG_COLOR_ERROR = new Color(255,224,224);
private JTextField tfLat;
private JTextField tfLon;
private boolean canceled = false;
private LatLon coordinates;
private OKAction actOK;
private CancelAction actCancel;
protected JPanel buildInputForm() {
JPanel pnl = new JPanel(new GridBagLayout());
pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
pnl.add(new JLabel("<html>"+
tr("Enter the coordinates for the new node.") +
"<br>" + tr("Use decimal degrees.") +
"<br>" + tr("Negative values denote Western/Southern hemisphere.")),
GBC.eol());
pnl.add(new JLabel(tr("Latitude")), GBC.std().insets(0,10,5,0));
tfLat = new JTextField(12);
pnl.add(tfLat, GBC.eol().insets(0,10,0,0));
pnl.add(new JLabel(tr("Longitude")), GBC.std().insets(0,0,5,10));
tfLon = new JTextField(12);
pnl.add(tfLon, GBC.eol().insets(0,0,0,10));
// parse and verify input on the fly
//
LatLonInputVerifier inputVerifier = new LatLonInputVerifier();
tfLat.getDocument().addDocumentListener(inputVerifier);
tfLon.getDocument().addDocumentListener(inputVerifier);
// select the text in the field on focus
//
TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
tfLat.addFocusListener(focusHandler);
tfLon.addFocusListener(focusHandler);
return pnl;
}
protected JPanel buildButtonRow() {
JPanel pnl = new JPanel(new FlowLayout());
SideButton btn;
pnl.add(btn = new SideButton(actOK = new OKAction()));
makeButtonRespondToEnter(btn);
pnl.add(btn = new SideButton(actCancel = new CancelAction()));
makeButtonRespondToEnter(btn);
pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Action/AddNode"))));
return pnl;
}
protected void makeButtonRespondToEnter(SideButton btn) {
btn.setFocusable(true);
btn.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter");
btn.getActionMap().put("enter", btn.getAction());
}
protected void build() {
getContentPane().setLayout(new BorderLayout());
getContentPane().add(buildInputForm(), BorderLayout.CENTER);
getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
pack();
// make dialog respond to ESCAPE
//
getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "escape");
getRootPane().getActionMap().put("escape", actCancel);
// make dialog respond to F1
//
HelpUtil.setHelpContext(getRootPane(), ht("/Action/AddNode"));
}
public LatLonDialog(Component parent) {
super(JOptionPane.getFrameForComponent(parent), true /* modal */);
setTitle(tr("Add Node..."));
build();
addWindowListener(new WindowEventHandler());
setCoordinates(null);
}
public void setCoordinates(LatLon coordinates) {
if (coordinates == null) {
coordinates = new LatLon(0,0);
}
this.coordinates = coordinates;
tfLat.setText(coordinates.latToString(CoordinateFormat.DECIMAL_DEGREES));
tfLon.setText(coordinates.lonToString(CoordinateFormat.DECIMAL_DEGREES));
actOK.setEnabled(true);
}
public LatLon getCoordinates() {
return coordinates;
}
protected void setErrorFeedback(JTextField tf, String message) {
tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
tf.setToolTipText(message);
tf.setBackground(BG_COLOR_ERROR);
}
protected void clearErrorFeedback(JTextField tf, String message) {
tf.setBorder(UIManager.getBorder("TextField.border"));
tf.setToolTipText(message);
tf.setBackground(UIManager.getColor("TextField.background"));
}
protected Double parseDoubleFromUserInput(String input) {
if (input == null) return null;
// remove white space and an optional degree symbol
//
input = input.trim();
input = input.replaceAll("\u00B0", ""); // the degree symbol
// try to parse using the current locale
//
NumberFormat f = NumberFormat.getNumberInstance();
Number n=null;
ParsePosition pp = new ParsePosition(0);
n = f.parse(input,pp);
if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length()) {
// fall back - try to parse with the english locale
//
pp = new ParsePosition(0);
f = NumberFormat.getNumberInstance(Locale.ENGLISH);
n = f.parse(input, pp);
if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length())
return null;
}
return n== null ? null : n.doubleValue();
}
protected Double parseLatFromUserInput() {
Double d = parseDoubleFromUserInput(tfLat.getText());
if (d == null || ! LatLon.isValidLat(d)) {
setErrorFeedback(tfLat, tr("Please enter a valid latitude in the range -90..90"));
return null;
} else {
clearErrorFeedback(tfLat,tr("Please enter a latitude in the range -90..90"));
}
return d;
}
protected Double parseLonFromUserInput() {
Double d = parseDoubleFromUserInput(tfLon.getText());
if (d == null || ! LatLon.isValidLon(d)) {
setErrorFeedback(tfLon, tr("Please enter a valid longitude in the range -180..180"));
return null;
} else {
clearErrorFeedback(tfLon,tr("Please enter a longitude in the range -180..180"));
}
return d;
}
protected void parseUserInput() {
Double lat = parseLatFromUserInput();
Double lon = parseLonFromUserInput();
if (lat == null || lon == null) {
coordinates = null;
actOK.setEnabled(false);
} else {
coordinates = new LatLon(lat,lon);
actOK.setEnabled(true);
}
}
public boolean isCanceled() {
return canceled;
}
protected void setCanceled(boolean canceled) {
this.canceled = canceled;
}
@Override
public void setVisible(boolean visible) {
if (visible) {
setCanceled(false);
WindowGeometry.centerInWindow(Main.parent, getSize()).applySafe(this);
}
super.setVisible(visible);
}
class OKAction extends AbstractAction {
public OKAction() {
putValue(NAME, tr("OK"));
putValue(SHORT_DESCRIPTION, tr("Close the dialog and create a new node"));
putValue(SMALL_ICON, ImageProvider.get("ok"));
}
public void actionPerformed(ActionEvent e) {
setCanceled(false);
setVisible(false);
}
}
class CancelAction extends AbstractAction {
public CancelAction() {
putValue(NAME, tr("Cancel"));
putValue(SHORT_DESCRIPTION, tr("Close the dialog, do not create a new node"));
putValue(SMALL_ICON, ImageProvider.get("cancel"));
}
public void actionPerformed(ActionEvent e) {
setCanceled(true);
setVisible(false);
}
}
class LatLonInputVerifier implements DocumentListener {
public void changedUpdate(DocumentEvent e) {
parseUserInput();
}
public void insertUpdate(DocumentEvent e) {
parseUserInput();
}
public void removeUpdate(DocumentEvent e) {
parseUserInput();
}
}
static class TextFieldFocusHandler implements FocusListener {
public void focusGained(FocusEvent e) {
Component c = e.getComponent();
if (c instanceof JTextField) {
JTextField tf = (JTextField)c;
tf.selectAll();
}
}
public void focusLost(FocusEvent e) {}
}
class WindowEventHandler extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
setCanceled(true);
setVisible(false);
}
@Override
public void windowOpened(WindowEvent e) {
tfLat.requestFocusInWindow();
}
}
}
}