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