001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.download;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.GridBagLayout;
010import java.awt.GridLayout;
011import java.awt.event.ActionEvent;
012import java.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.io.IOException;
015import java.io.Reader;
016import java.net.URL;
017import java.text.DecimalFormat;
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.List;
021import java.util.StringTokenizer;
022
023import javax.swing.AbstractAction;
024import javax.swing.BorderFactory;
025import javax.swing.DefaultListSelectionModel;
026import javax.swing.JButton;
027import javax.swing.JLabel;
028import javax.swing.JOptionPane;
029import javax.swing.JPanel;
030import javax.swing.JScrollPane;
031import javax.swing.JTable;
032import javax.swing.ListSelectionModel;
033import javax.swing.UIManager;
034import javax.swing.event.DocumentEvent;
035import javax.swing.event.DocumentListener;
036import javax.swing.event.ListSelectionEvent;
037import javax.swing.event.ListSelectionListener;
038import javax.swing.table.DefaultTableColumnModel;
039import javax.swing.table.DefaultTableModel;
040import javax.swing.table.TableCellRenderer;
041import javax.swing.table.TableColumn;
042import javax.xml.parsers.ParserConfigurationException;
043
044import org.openstreetmap.josm.data.Bounds;
045import org.openstreetmap.josm.gui.ExceptionDialogUtil;
046import org.openstreetmap.josm.gui.HelpAwareOptionPane;
047import org.openstreetmap.josm.gui.MainApplication;
048import org.openstreetmap.josm.gui.PleaseWaitRunnable;
049import org.openstreetmap.josm.gui.util.GuiHelper;
050import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
051import org.openstreetmap.josm.gui.widgets.JosmComboBox;
052import org.openstreetmap.josm.io.NameFinder;
053import org.openstreetmap.josm.io.NameFinder.SearchResult;
054import org.openstreetmap.josm.io.OsmTransferException;
055import org.openstreetmap.josm.spi.preferences.Config;
056import org.openstreetmap.josm.tools.GBC;
057import org.openstreetmap.josm.tools.HttpClient;
058import org.openstreetmap.josm.tools.ImageProvider;
059import org.openstreetmap.josm.tools.Logging;
060import org.openstreetmap.josm.tools.Utils;
061import org.xml.sax.SAXException;
062import org.xml.sax.SAXParseException;
063
064/**
065 * Place selector.
066 * @since 1329
067 */
068public class PlaceSelection implements DownloadSelection {
069    private static final String HISTORY_KEY = "download.places.history";
070
071    private HistoryComboBox cbSearchExpression;
072    private NamedResultTableModel model;
073    private NamedResultTableColumnModel columnmodel;
074    private JTable tblSearchResults;
075    private DownloadDialog parent;
076    private static final Server[] SERVERS = {
077        new Server("Nominatim", NameFinder.NOMINATIM_URL, tr("Class Type"), tr("Bounds"))
078    };
079    private final JosmComboBox<Server> server = new JosmComboBox<>(SERVERS);
080
081    private static class Server {
082        public final String name;
083        public final String url;
084        public final String thirdcol;
085        public final String fourthcol;
086
087        Server(String n, String u, String t, String f) {
088            name = n;
089            url = u;
090            thirdcol = t;
091            fourthcol = f;
092        }
093
094        @Override
095        public String toString() {
096            return name;
097        }
098    }
099
100    protected JPanel buildSearchPanel() {
101        JPanel lpanel = new JPanel(new GridLayout(2, 2));
102        JPanel panel = new JPanel(new GridBagLayout());
103
104        lpanel.add(new JLabel(tr("Choose the server for searching:")));
105        lpanel.add(server);
106        String s = Config.getPref().get("namefinder.server", SERVERS[0].name);
107        for (int i = 0; i < SERVERS.length; ++i) {
108            if (SERVERS[i].name.equals(s)) {
109                server.setSelectedIndex(i);
110            }
111        }
112        lpanel.add(new JLabel(tr("Enter a place name to search for:")));
113
114        cbSearchExpression = new HistoryComboBox();
115        cbSearchExpression.setToolTipText(tr("Enter a place name to search for"));
116        cbSearchExpression.setPossibleItemsTopDown(Config.getPref().getList(HISTORY_KEY, Collections.emptyList()));
117        lpanel.add(cbSearchExpression);
118
119        panel.add(lpanel, GBC.std().fill(GBC.HORIZONTAL).insets(5, 5, 0, 5));
120        SearchAction searchAction = new SearchAction();
121        JButton btnSearch = new JButton(searchAction);
122        cbSearchExpression.getEditorComponent().getDocument().addDocumentListener(searchAction);
123        cbSearchExpression.getEditorComponent().addActionListener(searchAction);
124
125        panel.add(btnSearch, GBC.eol().insets(5, 5, 0, 5));
126
127        return panel;
128    }
129
130    /**
131     * Adds a new tab to the download dialog in JOSM.
132     *
133     * This method is, for all intents and purposes, the constructor for this class.
134     */
135    @Override
136    public void addGui(final DownloadDialog gui) {
137        JPanel panel = new JPanel(new BorderLayout());
138        panel.add(buildSearchPanel(), BorderLayout.NORTH);
139
140        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
141        model = new NamedResultTableModel(selectionModel);
142        columnmodel = new NamedResultTableColumnModel();
143        tblSearchResults = new JTable(model, columnmodel);
144        tblSearchResults.setSelectionModel(selectionModel);
145        JScrollPane scrollPane = new JScrollPane(tblSearchResults);
146        scrollPane.setPreferredSize(new Dimension(200, 200));
147        panel.add(scrollPane, BorderLayout.CENTER);
148
149        if (gui != null)
150            gui.addDownloadAreaSelector(panel, tr("Areas around places"));
151
152        scrollPane.setPreferredSize(scrollPane.getPreferredSize());
153        tblSearchResults.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
154        tblSearchResults.getSelectionModel().addListSelectionListener(new ListSelectionHandler());
155        tblSearchResults.addMouseListener(new MouseAdapter() {
156            @Override
157            public void mouseClicked(MouseEvent e) {
158                if (e.getClickCount() > 1) {
159                    SearchResult sr = model.getSelectedSearchResult();
160                    if (sr != null) {
161                        parent.startDownload(sr.getDownloadArea());
162                    }
163                }
164            }
165        });
166        parent = gui;
167    }
168
169    @Override
170    public void setDownloadArea(Bounds area) {
171        tblSearchResults.clearSelection();
172    }
173
174    class SearchAction extends AbstractAction implements DocumentListener {
175
176        SearchAction() {
177            putValue(NAME, tr("Search..."));
178            new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
179            putValue(SHORT_DESCRIPTION, tr("Click to start searching for places"));
180            updateEnabledState();
181        }
182
183        @Override
184        public void actionPerformed(ActionEvent e) {
185            if (!isEnabled() || cbSearchExpression.getText().trim().isEmpty())
186                return;
187            cbSearchExpression.addCurrentItemToHistory();
188            Config.getPref().putList(HISTORY_KEY, cbSearchExpression.getHistory());
189            NameQueryTask task = new NameQueryTask(cbSearchExpression.getText());
190            MainApplication.worker.submit(task);
191        }
192
193        protected final void updateEnabledState() {
194            setEnabled(!cbSearchExpression.getText().trim().isEmpty());
195        }
196
197        @Override
198        public void changedUpdate(DocumentEvent e) {
199            updateEnabledState();
200        }
201
202        @Override
203        public void insertUpdate(DocumentEvent e) {
204            updateEnabledState();
205        }
206
207        @Override
208        public void removeUpdate(DocumentEvent e) {
209            updateEnabledState();
210        }
211    }
212
213    class NameQueryTask extends PleaseWaitRunnable {
214
215        private final String searchExpression;
216        private HttpClient connection;
217        private List<SearchResult> data;
218        private boolean canceled;
219        private final Server useserver;
220        private Exception lastException;
221
222        NameQueryTask(String searchExpression) {
223            super(tr("Querying name server"), false /* don't ignore exceptions */);
224            this.searchExpression = searchExpression;
225            useserver = (Server) server.getSelectedItem();
226            Config.getPref().put("namefinder.server", useserver.name);
227        }
228
229        @Override
230        protected void cancel() {
231            this.canceled = true;
232            synchronized (this) {
233                if (connection != null) {
234                    connection.disconnect();
235                }
236            }
237        }
238
239        @Override
240        protected void finish() {
241            if (canceled)
242                return;
243            if (lastException != null) {
244                ExceptionDialogUtil.explainException(lastException);
245                return;
246            }
247            columnmodel.setHeadlines(useserver.thirdcol, useserver.fourthcol);
248            model.setData(this.data);
249        }
250
251        @Override
252        protected void realRun() throws SAXException, IOException, OsmTransferException {
253            String urlString = useserver.url+Utils.encodeUrl(searchExpression);
254
255            try {
256                getProgressMonitor().indeterminateSubTask(tr("Querying name server ..."));
257                URL url = new URL(urlString);
258                synchronized (this) {
259                    connection = HttpClient.create(url);
260                    connection.connect();
261                }
262                try (Reader reader = connection.getResponse().getContentReader()) {
263                    data = NameFinder.parseSearchResults(reader);
264                }
265            } catch (SAXParseException e) {
266                if (!canceled) {
267                    // Nominatim sometimes returns garbage, see #5934, #10643
268                    Logging.log(Logging.LEVEL_WARN, tr("Error occurred with query ''{0}'': ''{1}''", urlString, e.getMessage()), e);
269                    GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog(
270                            MainApplication.getMainFrame(),
271                            tr("Name server returned invalid data. Please try again."),
272                            tr("Bad response"),
273                            JOptionPane.WARNING_MESSAGE, null
274                    ));
275                }
276            } catch (IOException | ParserConfigurationException e) {
277                if (!canceled) {
278                    OsmTransferException ex = new OsmTransferException(e);
279                    ex.setUrl(urlString);
280                    lastException = ex;
281                }
282            }
283        }
284    }
285
286    static class NamedResultTableModel extends DefaultTableModel {
287        private transient List<SearchResult> data;
288        private final transient ListSelectionModel selectionModel;
289
290        NamedResultTableModel(ListSelectionModel selectionModel) {
291            data = new ArrayList<>();
292            this.selectionModel = selectionModel;
293        }
294
295        @Override
296        public int getRowCount() {
297            return data != null ? data.size() : 0;
298        }
299
300        @Override
301        public Object getValueAt(int row, int column) {
302            return data != null ? data.get(row) : null;
303        }
304
305        public void setData(List<SearchResult> data) {
306            if (data == null) {
307                this.data.clear();
308            } else {
309                this.data = new ArrayList<>(data);
310            }
311            fireTableDataChanged();
312        }
313
314        @Override
315        public boolean isCellEditable(int row, int column) {
316            return false;
317        }
318
319        public SearchResult getSelectedSearchResult() {
320            if (selectionModel.getMinSelectionIndex() < 0)
321                return null;
322            return data.get(selectionModel.getMinSelectionIndex());
323        }
324    }
325
326    static class NamedResultTableColumnModel extends DefaultTableColumnModel {
327        private TableColumn col3;
328        private TableColumn col4;
329
330        NamedResultTableColumnModel() {
331            createColumns();
332        }
333
334        protected final void createColumns() {
335            TableColumn col;
336            NamedResultCellRenderer renderer = new NamedResultCellRenderer();
337
338            // column 0 - Name
339            col = new TableColumn(0);
340            col.setHeaderValue(tr("Name"));
341            col.setResizable(true);
342            col.setPreferredWidth(200);
343            col.setCellRenderer(renderer);
344            addColumn(col);
345
346            // column 1 - Version
347            col = new TableColumn(1);
348            col.setHeaderValue(tr("Type"));
349            col.setResizable(true);
350            col.setPreferredWidth(100);
351            col.setCellRenderer(renderer);
352            addColumn(col);
353
354            // column 2 - Near
355            col3 = new TableColumn(2);
356            col3.setHeaderValue(SERVERS[0].thirdcol);
357            col3.setResizable(true);
358            col3.setPreferredWidth(100);
359            col3.setCellRenderer(renderer);
360            addColumn(col3);
361
362            // column 3 - Zoom
363            col4 = new TableColumn(3);
364            col4.setHeaderValue(SERVERS[0].fourthcol);
365            col4.setResizable(true);
366            col4.setPreferredWidth(50);
367            col4.setCellRenderer(renderer);
368            addColumn(col4);
369        }
370
371        public void setHeadlines(String third, String fourth) {
372            col3.setHeaderValue(third);
373            col4.setHeaderValue(fourth);
374            fireColumnMarginChanged();
375        }
376    }
377
378    class ListSelectionHandler implements ListSelectionListener {
379        @Override
380        public void valueChanged(ListSelectionEvent lse) {
381            SearchResult r = model.getSelectedSearchResult();
382            if (r != null) {
383                parent.boundingBoxChanged(r.getDownloadArea(), PlaceSelection.this);
384            }
385        }
386    }
387
388    static class NamedResultCellRenderer extends JLabel implements TableCellRenderer {
389
390        /**
391         * Constructs a new {@code NamedResultCellRenderer}.
392         */
393        NamedResultCellRenderer() {
394            setOpaque(true);
395            setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
396        }
397
398        protected void reset() {
399            setText("");
400            setIcon(null);
401        }
402
403        protected void renderColor(boolean selected) {
404            if (selected) {
405                setForeground(UIManager.getColor("Table.selectionForeground"));
406                setBackground(UIManager.getColor("Table.selectionBackground"));
407            } else {
408                setForeground(UIManager.getColor("Table.foreground"));
409                setBackground(UIManager.getColor("Table.background"));
410            }
411        }
412
413        protected String lineWrapDescription(String description) {
414            StringBuilder ret = new StringBuilder();
415            StringBuilder line = new StringBuilder();
416            StringTokenizer tok = new StringTokenizer(description, " ");
417            while (tok.hasMoreElements()) {
418                String t = tok.nextToken();
419                if (line.length() == 0) {
420                    line.append(t);
421                } else if (line.length() < 80) {
422                    line.append(' ').append(t);
423                } else {
424                    line.append(' ').append(t).append("<br>");
425                    ret.append(line);
426                    line = new StringBuilder();
427                }
428            }
429            ret.insert(0, "<html>");
430            ret.append("</html>");
431            return ret.toString();
432        }
433
434        @Override
435        public Component getTableCellRendererComponent(JTable table, Object value,
436                boolean isSelected, boolean hasFocus, int row, int column) {
437
438            reset();
439            renderColor(isSelected);
440
441            if (value == null)
442                return this;
443            SearchResult sr = (SearchResult) value;
444            switch(column) {
445            case 0:
446                setText(sr.getName());
447                break;
448            case 1:
449                setText(sr.getInfo());
450                break;
451            case 2:
452                setText(sr.getNearestPlace());
453                break;
454            case 3:
455                if (sr.getBounds() != null) {
456                    setText(sr.getBounds().toShortString(new DecimalFormat("0.000")));
457                } else {
458                    setText(sr.getZoom() != 0 ? Integer.toString(sr.getZoom()) : tr("unknown"));
459                }
460                break;
461            default: // Do nothing
462            }
463            setToolTipText(lineWrapDescription(sr.getDescription()));
464            return this;
465        }
466    }
467}