I'm trying to implement a custom JComboBox with a lookup on Google Geocoder API when the user hit VK_ENTER into the editable JComboBox.
Here the code:
package lucasepe.desktop.arsenal.widgets;
import java.awt.Component;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.SwingConstants;
import javax.swing.SwingWorker;
import lucasepe.desktop.arsenal.models.Address;
import lucasepe.desktop.arsenal.models.Location;
import lucasepe.desktop.arsenal.utils.IOUtils;
import lucasepe.desktop.enroll.utils.UIBundle;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;
@SuppressWarnings("serial")
public class GeocoderComboBox extends JComboBox<Location> {
static final
private Logger LOG = Logger.getLogger(GeocoderComboBox.class);
private DefaultComboBoxModel<Location> mAdapter;
public GeocoderComboBox() {
super();
mAdapter = new DefaultComboBoxModel<Location>();
setModel(mAdapter);
setEditable(true);
getEditor().getEditorComponent()
.addKeyListener(new LocationSearcher());
setRenderer(new LocationListCellRenderer());
}
private class LocationSearcher extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
String constraint = ((JTextField)getEditor().getEditorComponent()).getText();
GeocodeWorker worker = new GeocodeWorker(constraint);
worker.execute();
}
}
};
private class GeocodeWorker extends SwingWorker<List<Location>, Void> {
final
private String mConstraint;
public GeocodeWorker(String constraint) {
mConstraint = (constraint != null) ?
constraint.toString().trim() : null;
}
@Override
protected List<Location> doInBackground() throws Exception {
if (mConstraint == null || mConstraint.length() == 0)
return null;
StringBuilder url = new StringBuilder();
url.append("https://maps.googleapis.com/maps/api/geocode/json?");
url.append("language=it");
url.append("&components=")
.append(encode("country:IT|route:")).append(encode(mConstraint));
url.append("&key=").append(UIBundle.getString("google_api_key"));
ArrayList<Location> locations = new ArrayList<Location>(10);
try {
JSONObject response = IOUtils.fetchJSONObject(url.toString());
if (response != null && response.optString("status", "KO").equals("OK")) {
JSONArray ja = response.getJSONArray("results");
for (int i = 0; i < ja.length(); i++) {
JSONObject jo = ja.getJSONObject(i);
String address = jo.optString("formatted_address");
if ((address != null) && address.trim().length() > 0) {
Address poi = new Address("GOOGLE");
LOG.debug("jo: " + jo.toString());
poi.setTitle(address);
poi.setCity(getShortName("locality", jo));
JSONObject geo = jo.getJSONObject("geometry")
.getJSONObject("location");
poi.setLatitude(geo.getDouble("lat"));
poi.setLongitude(geo.getDouble("lng"));
locations.add(poi);
}
}
}
} catch (Exception err) {
LOG.error("error: " + err.getMessage(), err);
}
return locations;
}
@Override
protected void done() {
try {
List<Location> data = get();
if (data == null || data.size() == 0)
mAdapter.removeAllElements();
else {
for (Location l: data)
mAdapter.addElement(l);
}
} catch (Exception err) {
LOG.error("error: " + err.getMessage(), err);
}
}
private String getShortName(String type, JSONObject jo) {
if (type == null || type.length() == 0 || jo == null)
return null;
String result = null;
try{
JSONArray ja = jo.optJSONArray("types");
if (ja != null) {
for (int i = 0; i < ja.length(); i++) {
if (ja.getString(i).equals(type)) {
result = jo.optString("short_name");
break;
}
}
}
} catch (Exception err) {
LOG.error("err: " + err.getMessage());
}
return result;
}
private String encode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException err) {
LOG.error("encoding: " + value, err);
return value;
}
}
};
private class LocationListCellRenderer extends JPanel
implements
ListCellRenderer<Location> {
private JLabel mDisplayName;
public LocationListCellRenderer() {
super();
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
setLayout(new VerticalFlowLayout());
mDisplayName = new JLabel();
mDisplayName.setHorizontalAlignment(SwingConstants.LEFT);
add(mDisplayName);
}
@Override
public Component getListCellRendererComponent(
JList<? extends Location> list, Location value, int index,
boolean isSelected, boolean cellHasFocus) {
mDisplayName.setText(value.toString());
return this;
}
};
}
This is how I'm using it:
package lucasepe.desktop.enroll.fragments;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerDateModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import lucasepe.desktop.arsenal.models.Address;
import lucasepe.desktop.arsenal.models.Location;
import lucasepe.desktop.arsenal.widgets.GeocoderComboBox;
import lucasepe.desktop.enroll.database.EnrollContract.PersonaColumns;
import net.java.dev.designgridlayout.DesignGridLayout;
import net.java.dev.designgridlayout.LabelAlignment;
import org.apache.log4j.PropertyConfigurator;
@SuppressWarnings("serial")
public class EditPersonaFragment extends JPanel {
private JTextField mFirstName;
private JTextField mLastName;
private JTextField mPhone;
private JTextField mEmail;
private JSpinner mBirthDate;
private JComboBox<PersonaColumns.Gender> mGender;
//private JTextField mAddress;
private GeocoderComboBox mAddress;
private JTextField mPostalCode;
private JTextField mCity;
private JTextField mState;
private JTextField mCountry;
public EditPersonaFragment() {
super();
mFirstName = new JTextField();
mLastName = new JTextField();
mPhone = new JTextField();
mEmail = new JTextField();
mBirthDate = new JSpinner();
mBirthDate.setModel(new SpinnerDateModel());
mBirthDate.setEditor(new JSpinner.DateEditor(mBirthDate, "dd/MM/yyyy"));
mGender = new JComboBox<PersonaColumns.Gender>(PersonaColumns.Gender.values());
//mAddress = new JTextField();
mAddress = new GeocoderComboBox();
mAddress.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent event) {
if (event.getStateChange() == ItemEvent.SELECTED) {
Address item = (Address)mAddress.getSelectedItem();
mCity.setText(item.getCity());
mState.setText(item.getState());
mCountry.setText(item.getCountry());
}
}
});
mCity = new JTextField();;
mState = new JTextField();
mCountry = new JTextField();
mPostalCode = new JTextField();
DesignGridLayout layout = new DesignGridLayout(this);
layout.labelAlignment(LabelAlignment.RIGHT);
layout.row()
.grid(new JLabel("Last Name")).add(mLastName)
.grid(new JLabel("First Name")).add(mFirstName);
layout.row()
.grid(new JLabel("Phone")).add(mPhone)
.grid(new JLabel("Email")).add(mEmail);
layout.row()
.grid(new JLabel("Birthdate")).add(mBirthDate)
.grid(new JLabel("Gender")).add(mGender);
layout.emptyRow();layout.emptyRow();
layout.row().center().fill().add(new JSeparator());
layout.emptyRow();layout.emptyRow();
layout.row().grid(new JLabel("Address")).add(mAddress);
layout.row()
.grid(new JLabel("City")).add(mCity)
.grid(new JLabel("Postal Code")).add(mPostalCode);
layout.row()
.grid(new JLabel("State")).add(mState)
.grid(new JLabel("Country")).add(mCountry);
}
public static void main(String[] args) throws Exception {
PropertyConfigurator.configure("logger.properties");
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("Login Form");
frame.getContentPane().add(new EditPersonaFragment());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
});
}
}
The very first time I enter an address and press enter, the Geocoder works but get this exception:
java.lang.String cannot be cast to lucasepe.desktop.arsenal.models.Address
at lucasepe.desktop.enroll.fragments.EditPersonaFragment$1.itemStateChanged(EditPersonaFragment.java:66)
looks like ItemListener is fired: "How avoid this?"
There is a better way to fire the geocoder when an user hit enter on the editable JComboBox?
I'm thinking that I'm doing this wrong, maybe someone could be so kind to point me in the right direction.
P.S.
I'm an Android guy who is willing to learn the Desktop way, so please don't hit me too hard :-)
As I can see after the worker's job is done you remove all options from the combobox model and add results.
You should skip the events in your listener either by removing the listener before model updating and readding after or by introducing a flag (let's call is isAPI)=false by default. In your listener you can check the flag and process selection only if the isAPI=false. Before updating model set the flag to true and reset it back after updating.