Search code examples
javaswingautocompletejcombobox

JComboBox with lookup at Geocoder Google APIs


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 :-)


Solution

  • 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.