// License: GPL. For details, see LICENSE file.
package terracer;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingComboBox;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionListItem;
import org.openstreetmap.josm.tools.UserCancelException;
/**
* The Class HouseNumberInputHandler contains all the logic
* behind the house number input dialog.
*
* From a refactoring viewpoint, this class is indeed more interested in the fields
* of the HouseNumberInputDialog. This is desired design, as the HouseNumberInputDialog
* is already cluttered with auto-generated layout code.
*
* @author casualwalker - Copyright 2009 CloudMade Ltd
*/
public class HouseNumberInputHandler extends JosmAction implements ActionListener, FocusListener, ItemListener {
private final TerracerAction terracerAction;
private final Way outline, street;
private final String streetName;
private final Node init;
private final Relation associatedStreet;
private final ArrayList<Node> housenumbers;
public HouseNumberInputDialog dialog;
/**
* Instantiates a new house number input handler.
*
* @param terracerAction the terracer action
* @param outline the closed, quadrilateral way to terrace.
* @param init The node that hints at which side to start the numbering
* @param street the street, the buildings belong to (may be null)
* @param streetName the name of the street, derived from either the street line or
* the house numbers which are guaranteed to have the same name
* attached (may be null)
* @param buildingType The value to add for building key
* @param associatedStreet a relation where we can add the houses (may be null)
* @param housenumbers a list of house number nodes in this outline (may be empty)
* @param title the title
*/
public HouseNumberInputHandler(final TerracerAction terracerAction,
final Way outline, final Node init, final Way street, final String streetName, final String buildingType,
final Relation associatedStreet,
final ArrayList<Node> housenumbers, final String title) {
this.terracerAction = terracerAction;
this.outline = outline;
this.init = init;
this.street = street;
this.streetName = streetName;
this.associatedStreet = associatedStreet;
this.housenumbers = housenumbers;
this.dialog = new HouseNumberInputDialog(this, street, streetName, buildingType,
associatedStreet != null, housenumbers);
}
/**
* Find a button with a certain caption.
* Loops recursively through all objects to find all buttons.
* Function returns on the first match.
*
* @param root A container object that is recursively searched for other containers or buttons
* @param caption The caption of the button that is being searched
*
* @return The first button that matches the caption or null if not found
*/
private static JButton getButton(Container root, String caption) {
Component[] children = root.getComponents();
for (Component child : children) {
JButton b;
if (child instanceof JButton) {
b = (JButton) child;
if (caption.equals(b.getText())) return b;
} else if (child instanceof Container) {
b = getButton((Container) child, caption);
if (b != null) return b;
}
}
return null;
}
/**
* Validate the current input fields.
* When the validation fails, a red message is
* displayed and the OK button is disabled.
*
* Should be triggered each time the input changes.
*/
private boolean validateInput() {
boolean isOk = true;
StringBuffer message = new StringBuffer();
isOk = isOk && checkNumberOrder(message);
isOk = isOk && checkSegmentsFromHousenumber(message);
isOk = isOk && checkSegments(message);
// Allow non numeric characters for the low number as long as there is
// no high number of the segmentcount is 1
if (dialog.hi.getText().length() > 0 && (segments() != null || segments() < 1)) {
isOk = isOk
&& checkNumberStringField(dialog.lo, tr("Lowest number"),
message);
}
isOk = isOk
&& checkNumberStringField(dialog.hi, tr("Highest number"),
message);
isOk = isOk
&& checkNumberStringField(dialog.segments, tr("Segments"),
message);
if (isOk) {
JButton okButton = getButton(dialog, "OK");
if (okButton != null)
okButton.setEnabled(true);
// For some reason the messageLabel doesn't want to show up
dialog.messageLabel.setForeground(Color.black);
dialog.messageLabel.setText(tr(HouseNumberInputDialog.DEFAULT_MESSAGE));
return true;
} else {
JButton okButton = getButton(dialog, "OK");
if (okButton != null)
okButton.setEnabled(false);
// For some reason the messageLabel doesn't want to show up, so a
// MessageDialog is shown instead. Someone more knowledgeable might fix this.
dialog.messageLabel.setForeground(Color.red);
dialog.messageLabel.setText(message.toString());
// JOptionPane.showMessageDialog(null, message.toString(),
// tr("Error"), JOptionPane.ERROR_MESSAGE);
return false;
}
}
/**
* Checks, if the lowest house number is indeed lower than the
* highest house number.
* This check applies only, if the house number fields are used at all.
*
* @param message the message
*
* @return true, if successful
*/
private boolean checkNumberOrder(final StringBuffer message) {
if (numberFrom() != null && numberTo() != null) {
if (numberFrom().intValue() > numberTo().intValue()) {
appendMessageNewLine(message);
message.append(tr("Lowest housenumber cannot be higher than highest housenumber"));
return false;
}
}
return true;
}
/**
* Obtain the number segments from the house number fields and check,
* if they are valid.
*
* Also disables the segments field, if the house numbers contain
* valid information.
*
* @param message the message
*
* @return true, if successful
*/
private boolean checkSegmentsFromHousenumber(final StringBuffer message) {
if (!dialog.numbers.isVisible()) {
dialog.segments.setEditable(true);
if (numberFrom() != null && numberTo() != null) {
int segments = numberTo().intValue() - numberFrom().intValue();
if (segments % stepSize() != 0) {
appendMessageNewLine(message);
message
.append(tr("Housenumbers do not match odd/even setting"));
return false;
}
int steps = segments / stepSize();
steps++; // difference 0 means 1 building, see
// TerracerActon.terraceBuilding
dialog.segments.setText(String.valueOf(steps));
dialog.segments.setEditable(false);
}
}
return true;
}
/**
* Check the number of segments.
* It must be a number and greater than 1.
*
* @param message the message
*
* @return true, if successful
*/
private boolean checkSegments(final StringBuffer message) {
if (segments() == null || segments().intValue() < 1) {
appendMessageNewLine(message);
message.append(tr("Segment must be a number greater 1"));
return false;
}
return true;
}
/**
* Check, if a string field contains a positive integer.
*
* @param field the field
* @param label the label
* @param message the message
*
* @return true, if successful
*/
private boolean checkNumberStringField(final JTextField field,
final String label, final StringBuffer message) {
final String content = field.getText();
if (content != null && content.length() != 0) {
try {
int i = Integer.parseInt(content);
if (i < 0) {
appendMessageNewLine(message);
message.append(tr("{0} must be greater than 0", label));
return false;
}
} catch (NumberFormatException e) {
appendMessageNewLine(message);
message.append(tr("{0} is not a number", label));
return false;
}
}
return true;
}
/**
* Append a new line to the message, if the message is not empty.
*
* @param message the message
*/
private void appendMessageNewLine(final StringBuffer message) {
if (message.length() > 0) {
message.append("\n");
}
}
@Override
public void itemStateChanged(ItemEvent e) {
validateInput();
}
@Override
public void actionPerformed(final ActionEvent e) {
// OK or Cancel button-actions
if (e.getSource() instanceof JButton) {
JButton button = (JButton) e.getSource();
if (tr("OK").equals(button.getActionCommand()) & button.isEnabled()) {
if (validateInput()) {
saveValues();
try {
terracerAction.terraceBuilding(
outline,
init,
street,
associatedStreet,
segments(),
dialog.lo.getText(),
dialog.hi.getText(),
stepSize(),
housenumbers,
streetName(),
doHandleRelation(),
doKeepOutline(), buildingType());
} catch (UserCancelException ex) {
Main.trace(ex);
}
this.dialog.setVisible(false);
}
} else if (tr("Cancel").equals(button.getActionCommand())) {
this.dialog.setVisible(false);
}
} else {
// Anything else is a change in the input (we don't get here though)
validateInput();
}
}
/**
* Calculate the step size between two house numbers,
* based on the interpolation setting.
*
* @return the stepSize (1 for all, 2 for odd /even)
*/
public Integer stepSize() {
return (dialog.interpolation.getSelectedItem().equals(tr("All"))) ? 1 : 2;
}
/**
* Gets the number of segments, if set.
*
* @return the number of segments or null, if not set / invalid.
*/
public Integer segments() {
try {
return Integer.parseInt(dialog.segments.getText());
} catch (NumberFormatException ex) {
return null;
}
}
/**
* Gets the lowest house number.
*
* @return the number of lowest house number or null, if not set / invalid.
*/
public Integer numberFrom() {
try {
return Integer.parseInt(dialog.lo.getText());
} catch (NumberFormatException ex) {
return null;
}
}
/**
* Gets the highest house number.
*
* @return the number of highest house number or null, if not set / invalid.
*/
public Integer numberTo() {
try {
return Integer.parseInt(dialog.hi.getText());
} catch (NumberFormatException ex) {
return null;
}
}
/**
* Gets the street name.
*
* @return the street name or null, if not set / invalid.
*/
public String streetName() {
if (streetName != null)
return streetName;
return getItemText(dialog.streetComboBox);
}
/**
* Gets the building type.
*
* @return the building type or null, if not set / invalid.
*/
public String buildingType() {
return getItemText(dialog.buildingComboBox);
}
private static String getItemText(AutoCompletingComboBox box) {
Object selected = box.getSelectedItem();
if (selected == null) {
return null;
} else {
String name;
if (selected instanceof AutoCompletionListItem) {
name = ((AutoCompletionListItem) selected).getValue();
} else {
name = selected.toString();
}
if (name.length() == 0) {
return null;
} else {
return name;
}
}
}
/**
* Whether the user likes to create a relation or add to
* an existing one.
*/
public boolean doHandleRelation() {
if (this.dialog == null) {
JOptionPane.showMessageDialog(null, "dialog", "alert", JOptionPane.ERROR_MESSAGE);
}
if (this.dialog.handleRelationCheckBox == null) {
JOptionPane.showMessageDialog(null, "checkbox", "alert", JOptionPane.ERROR_MESSAGE);
return true;
} else {
return this.dialog.handleRelationCheckBox.isSelected();
}
}
/**
* Whether the user likes to keep the outline way.
*/
public boolean doKeepOutline() {
return dialog.keepOutlineCheckBox.isSelected();
}
@Override
public void focusGained(FocusEvent e) {
// Empty, but placeholder is required
}
@Override
public void focusLost(FocusEvent e) {
if (e.getOppositeComponent() == null)
return;
validateInput();
}
/**
* Saves settings.
*/
public void saveValues() {
Main.pref.put(HouseNumberInputDialog.HANDLE_RELATION, doHandleRelation());
Main.pref.put(HouseNumberInputDialog.KEEP_OUTLINE, doKeepOutline());
Main.pref.put(HouseNumberInputDialog.INTERPOLATION, stepSize().toString());
}
}