/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.studio.components.geocoder; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.TreeMap; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.opendoorlogistics.codefromweb.PackTableColumn; import com.opendoorlogistics.components.geocode.Countries.Country; import com.opendoorlogistics.core.utils.IntUtils; import com.opendoorlogistics.core.utils.strings.Strings; import com.opendoorlogistics.core.utils.ui.VerticalLayoutPanel; import com.opendoorlogistics.studio.components.geocoder.component.NominatimConfig; import com.opendoorlogistics.studio.components.geocoder.model.GeocodeModel; import com.opendoorlogistics.studio.components.geocoder.model.GeocodeModelListener; import com.opendoorlogistics.studio.components.geocoder.model.SearchResultPoint; import com.opendoorlogistics.studio.controls.EditableComboBox; import com.opendoorlogistics.studio.controls.EditableComboBox.ValueChangedListener; import com.opendoorlogistics.utils.ui.Icons; final public class SearchResultsPanel extends VerticalLayoutPanel implements GeocodeModelListener{ private final NominatimConfig config; private final GeocodeModel model; private final JButton refresh; private final EditableComboBox<String> serverBox; private final JComboBox<Country> countryCode; final JTable table; private final TreeMap<String, List<SearchResultPoint>> cache = new TreeMap<>(); private String lastQueryString=""; SearchResultsPanel(final NominatimConfig config,final GeocodeModel model) { this.model = model; this.config = config; // server selector at top serverBox =Controls.createServerBox(config.getServer()); serverBox.addValueChangedListener(new ValueChangedListener<String>() { @Override public void comboValueChanged(String newValue) { updateAppearance(); } }); countryCode =Controls.createCountryBox(config.getCountryCode()); countryCode.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { updateAppearance(); } }); // refresh button by server selector refresh = new JButton(Icons.loadFromStandardPath("view-refresh-6.png")); refresh.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { refreshResults(); } }); refresh.setToolTipText("Re-run the query"); // add the controls addLine(Controls.createServerLabel(),serverBox); addWhitespace(6); addLine(Controls.createCountryFilterLabel(), countryCode, Box.createRigidArea(new Dimension(10, 1)), refresh); addWhitespace(); // results table in the middle table = new JTable(); table.setColumnSelectionAllowed(false); table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { readSelectedIntoModel(); } }); table.setFillsViewportHeight(true); JScrollPane scrollPane = new JScrollPane(table); add(scrollPane); setTableModel(); model.addListener(this); updateAppearance(); } private void readSelectedIntoModel(){ model.setSelectedResultIndices(IntUtils.toArrayList(table.getSelectedRows())); } private String getQueryString() { ArrayList<BasicNameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("addressdetails", "1")); params.add(new BasicNameValuePair("limit", Integer.toString(NominatimConstants.RESULTS_LIMIT))); params.add(new BasicNameValuePair("format", "json")); params.add(new BasicNameValuePair("bounded", "0")); params.add(new BasicNameValuePair("q", model.getAddress())); // validate email... boolean okEmail = config.getEmail()!=null; if(okEmail){ String email = Strings.std(config.getEmail()); if(Strings.isEmailAddress(email)){ params.add(new BasicNameValuePair("email", config.getEmail())); }else{ okEmail = false; } } if(!okEmail){ throw new RuntimeException("Invalid email address entered: " + config.getEmail()); } Country country = (Country)countryCode.getSelectedItem(); if(country!=null && country!=Controls.ALL_COUNTRIES){ params.add(new BasicNameValuePair("countrycodes",country.getTwoDigitCode())); } String formatted = URLEncodedUtils.format(params, (String) null); String server = (String) serverBox.getEditor().getItem(); String uri = server + "?" + formatted; return uri; } private void refreshResults() { // build the query string including the server String uri = getQueryString(); // check for pre-existing results List<SearchResultPoint> cached = cache.get(uri); if (cached != null) { model.setSearchResults(Collections.unmodifiableList(cached)); } else { runQuery(uri); } lastQueryString = uri; setTableModel(); updateAppearance(); } private void updateAppearance(){ refresh.setEnabled(lastQueryString.equals(getQueryString())==false); } private void runQuery(String uri) { final ArrayList<SearchResultPoint> tmpResults = new ArrayList<SearchResultPoint>(); class Connected { boolean isConnected; } final Connected connected = new Connected(); CloseableHttpClient httpclient = HttpClients.createDefault(); try { HttpGet httpget = new HttpGet(uri); httpget.setHeader("User-Agent", "OpenDoorLogistics Studio, Nominatim client version 1.0"); // create response handler ResponseHandler<String> responseHandler = new ResponseHandler<String>() { public String handleResponse(final HttpResponse response) throws ClientProtocolException, IOException { int status = response.getStatusLine().getStatusCode(); if (status >= 200 && status < 300) { // flag that we did connect so we can report a sensible // error connected.isConnected = true; HttpEntity entity = response.getEntity(); String s = EntityUtils.toString(entity); ObjectMapper mapper = new ObjectMapper(); JsonNode rootNode = mapper.readValue(s, JsonNode.class); // // print to console // ObjectWriter writer = // mapper.writer().withDefaultPrettyPrinter(); // System.out.println(writer.writeValueAsString(rootNode) // + System.lineSeparator()); for (int i = 0; i < rootNode.size(); i++) { JsonNode child = rootNode.get(i); JsonNode name = child.get("display_name"); JsonNode lat = child.get("lat"); JsonNode lng = child.get("lon"); JsonNode cls = child.get("class"); JsonNode type = child.get("type"); JsonNode box = child.get("boundingbox"); // check result format OK; class and type can be // null boolean ok = name != null && lat != null && lng != null && box != null && box.size() == 4; double[] bx = new double[4]; for (int j = 0; j < 4 && ok; j++) { // String sText = box.get(j).textValue(); // ok = Strings.isNumber(sText); // if (ok) { bx[j] = box.get(j).asDouble(); //} } if (ok) { SearchResultPoint pnt = new SearchResultPoint(); pnt.setAddress(name.asText()); pnt.setLatitude(lat.asDouble()); pnt.setLongitude(lng.asDouble()); pnt.setLatLongRect(new Rectangle2D.Double(Math.min(bx[2], bx[3]), Math.min(bx[0], bx[1]), Math.abs(bx[3] - bx[2]), Math.abs(bx[1] - bx[2]))); if (cls != null) { pnt.setCls(cls.asText()); } if (type != null) { pnt.setType(type.asText()); } tmpResults.add(pnt); } else { throw new RuntimeException(); } } return null; } else { throw new RuntimeException(); } } }; httpclient.execute(httpget, responseHandler); // cache results cache.put(uri, Collections.unmodifiableList(tmpResults)); } catch (Throwable e) { if (connected.isConnected == false) { JOptionPane.showMessageDialog(this, "Error connecting to Nominatim webservice."); } else { JOptionPane.showMessageDialog(this, "Error parsing results returned by Nominatim webservice."); } } finally { model.setSearchResults(Collections.unmodifiableList(tmpResults)); try { httpclient.close(); } catch (IOException e) { throw new RuntimeException(e); } } } private void setTableModel() { final NumberFormat formatter = DecimalFormat.getInstance(); formatter.setMaximumFractionDigits(4); // update table table.setModel(new AbstractTableModel() { @Override public String getColumnName(int column) { switch (column) { case 0: return "Rank"; case 1: return "Address"; case 2: return "Latitude"; case 3: return "Longitude"; case 4: return "Class"; case 5: return "Type"; } throw new IndexOutOfBoundsException(); } public Class<?> getColumnClass(int columnIndex) { return String.class; } @Override public Object getValueAt(int rowIndex, int column) { SearchResultPoint pnt = model.getSearchResults().get(rowIndex); switch (column) { case 0: return Integer.toString(rowIndex + 1); case 1: return pnt.getAddress(); case 2: return formatter.format(pnt.getLatitude()); case 3: return formatter.format(pnt.getLongitude()); case 4: return pnt.getCls(); case 5: return pnt.getType(); } throw new IndexOutOfBoundsException(); } @Override public int getRowCount() { return model.getSearchResults()!=null ? model.getSearchResults().size():0; } @Override public int getColumnCount() { return 6; } }); PackTableColumn.packAll(table, 6); if (table.getModel().getRowCount() > 0) { table.getSelectionModel().setSelectionInterval(0, 0); } readSelectedIntoModel(); } @Override public void modelChanged(boolean recordChanged, boolean searchResultsChanged) { refresh.setEnabled(lastQueryString.equals(getQueryString())==false); if(recordChanged){ refreshResults(); } if(searchResultsChanged){ setTableModel(); } } // int getNbResults(){ // return results.size(); // } // // SearchResultPoint getResult(int i){ // return results.get(i); // } // interface OptionsChangedListener{ // void optionsChanged(); // } // public void addOptionsChangedListener(OptionsChangedListener optionsChangedListener) { // optionsChangedListeners.add(optionsChangedListener); // } // // List<ResultOption> getOptions(){ // return Collections.unmodifiableList(options); // } }