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.InputStream;
016import java.io.InputStreamReader;
017import java.net.HttpURLConnection;
018import java.net.URL;
019import java.text.DecimalFormat;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.StringTokenizer;
025
026import javax.swing.AbstractAction;
027import javax.swing.BorderFactory;
028import javax.swing.DefaultListSelectionModel;
029import javax.swing.JButton;
030import javax.swing.JLabel;
031import javax.swing.JPanel;
032import javax.swing.JScrollPane;
033import javax.swing.JTable;
034import javax.swing.JTextField;
035import javax.swing.ListSelectionModel;
036import javax.swing.UIManager;
037import javax.swing.event.DocumentEvent;
038import javax.swing.event.DocumentListener;
039import javax.swing.event.ListSelectionEvent;
040import javax.swing.event.ListSelectionListener;
041import javax.swing.table.DefaultTableColumnModel;
042import javax.swing.table.DefaultTableModel;
043import javax.swing.table.TableCellRenderer;
044import javax.swing.table.TableColumn;
045import javax.xml.parsers.SAXParserFactory;
046
047import org.openstreetmap.josm.Main;
048import org.openstreetmap.josm.data.Bounds;
049import org.openstreetmap.josm.gui.ExceptionDialogUtil;
050import org.openstreetmap.josm.gui.PleaseWaitRunnable;
051import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
052import org.openstreetmap.josm.gui.widgets.JosmComboBox;
053import org.openstreetmap.josm.io.OsmTransferException;
054import org.openstreetmap.josm.tools.GBC;
055import org.openstreetmap.josm.tools.ImageProvider;
056import org.openstreetmap.josm.tools.OsmUrlToBounds;
057import org.openstreetmap.josm.tools.Utils;
058import org.xml.sax.Attributes;
059import org.xml.sax.InputSource;
060import org.xml.sax.SAXException;
061import org.xml.sax.helpers.DefaultHandler;
062
063public class PlaceSelection implements DownloadSelection {
064    private static final String HISTORY_KEY = "download.places.history";
065
066    private HistoryComboBox cbSearchExpression;
067    private JButton btnSearch;
068    private NamedResultTableModel model;
069    private NamedResultTableColumnModel columnmodel;
070    private JTable tblSearchResults;
071    private DownloadDialog parent;
072    private final static Server[] servers = new Server[]{
073        new Server("Nominatim","http://nominatim.openstreetmap.org/search?format=xml&q=",tr("Class Type"),tr("Bounds")),
074        //new Server("Namefinder","http://gazetteer.openstreetmap.org/namefinder/search.xml?find=",tr("Near"),trc("placeselection", "Zoom"))
075    };
076    private final JosmComboBox server = new JosmComboBox(servers);
077
078    private static class Server {
079        public String name;
080        public String url;
081        public String thirdcol;
082        public String fourthcol;
083        @Override
084        public String toString() {
085            return name;
086        }
087        public Server(String n, String u, String t, String f) {
088            name = n;
089            url = u;
090            thirdcol = t;
091            fourthcol = f;
092        }
093    }
094
095    protected JPanel buildSearchPanel() {
096        JPanel lpanel = new JPanel();
097        lpanel.setLayout(new GridLayout(2,2));
098        JPanel panel = new JPanel();
099        panel.setLayout(new GridBagLayout());
100
101        lpanel.add(new JLabel(tr("Choose the server for searching:")));
102        lpanel.add(server);
103        String s = Main.pref.get("namefinder.server", servers[0].name);
104        for (int i = 0; i < servers.length; ++i) {
105            if (servers[i].name.equals(s)) {
106                server.setSelectedIndex(i);
107            }
108        }
109        lpanel.add(new JLabel(tr("Enter a place name to search for:")));
110
111        cbSearchExpression = new HistoryComboBox();
112        cbSearchExpression.setToolTipText(tr("Enter a place name to search for"));
113        List<String> cmtHistory = new LinkedList<String>(Main.pref.getCollection(HISTORY_KEY, new LinkedList<String>()));
114        Collections.reverse(cmtHistory);
115        cbSearchExpression.setPossibleItems(cmtHistory);
116        lpanel.add(cbSearchExpression);
117
118        panel.add(lpanel, GBC.std().fill(GBC.HORIZONTAL).insets(5, 5, 0, 5));
119        SearchAction searchAction = new SearchAction();
120        btnSearch = new JButton(searchAction);
121        ((JTextField)cbSearchExpression.getEditor().getEditorComponent()).getDocument().addDocumentListener(searchAction);
122        ((JTextField)cbSearchExpression.getEditor().getEditorComponent()).addActionListener(searchAction);
123
124        panel.add(btnSearch, GBC.eol().insets(5, 5, 0, 5));
125
126        return panel;
127    }
128
129    /**
130     * Adds a new tab to the download dialog in JOSM.
131     *
132     * This method is, for all intents and purposes, the constructor for this class.
133     */
134    @Override
135    public void addGui(final DownloadDialog gui) {
136        JPanel panel = new JPanel();
137        panel.setLayout(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        gui.addDownloadAreaSelector(panel, tr("Areas around places"));
150
151        scrollPane.setPreferredSize(scrollPane.getPreferredSize());
152        tblSearchResults.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
153        tblSearchResults.getSelectionModel().addListSelectionListener(new ListSelectionHandler());
154        tblSearchResults.addMouseListener(new MouseAdapter() {
155            @Override public void mouseClicked(MouseEvent e) {
156                if (e.getClickCount() > 1) {
157                    SearchResult sr = model.getSelectedSearchResult();
158                    if (sr == null) return;
159                    parent.startDownload(sr.getDownloadArea());
160                }
161            }
162        });
163        parent = gui;
164    }
165
166    @Override
167    public void setDownloadArea(Bounds area) {
168        tblSearchResults.clearSelection();
169    }
170
171    /**
172     * Data storage for search results.
173     */
174    static private class SearchResult {
175        public String name;
176        public String info;
177        public String nearestPlace;
178        public String description;
179        public double lat;
180        public double lon;
181        public int zoom = 0;
182        public Bounds bounds = null;
183
184        public Bounds getDownloadArea() {
185            return bounds != null ? bounds : OsmUrlToBounds.positionToBounds(lat, lon, zoom);
186        }
187    }
188
189    /**
190     * A very primitive parser for the name finder's output.
191     * Structure of xml described here:  http://wiki.openstreetmap.org/index.php/Name_finder
192     *
193     */
194    private static class NameFinderResultParser extends DefaultHandler {
195        private SearchResult currentResult = null;
196        private StringBuffer description = null;
197        private int depth = 0;
198        private List<SearchResult> data = new LinkedList<SearchResult>();
199
200        /**
201         * Detect starting elements.
202         *
203         */
204        @Override
205        public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
206        throws SAXException {
207            depth++;
208            try {
209                if (qName.equals("searchresults")) {
210                    // do nothing
211                } else if (qName.equals("named") && (depth == 2)) {
212                    currentResult = new PlaceSelection.SearchResult();
213                    currentResult.name = atts.getValue("name");
214                    currentResult.info = atts.getValue("info");
215                    if(currentResult.info != null) {
216                        currentResult.info = tr(currentResult.info);
217                    }
218                    currentResult.lat = Double.parseDouble(atts.getValue("lat"));
219                    currentResult.lon = Double.parseDouble(atts.getValue("lon"));
220                    currentResult.zoom = Integer.parseInt(atts.getValue("zoom"));
221                    data.add(currentResult);
222                } else if (qName.equals("description") && (depth == 3)) {
223                    description = new StringBuffer();
224                } else if (qName.equals("named") && (depth == 4)) {
225                    // this is a "named" place in the nearest places list.
226                    String info = atts.getValue("info");
227                    if ("city".equals(info) || "town".equals(info) || "village".equals(info)) {
228                        currentResult.nearestPlace = atts.getValue("name");
229                    }
230                } else if (qName.equals("place") && atts.getValue("lat") != null) {
231                    currentResult = new PlaceSelection.SearchResult();
232                    currentResult.name = atts.getValue("display_name");
233                    currentResult.description = currentResult.name;
234                    currentResult.info = atts.getValue("class");
235                    if (currentResult.info != null) {
236                        currentResult.info = tr(currentResult.info);
237                    }
238                    currentResult.nearestPlace = tr(atts.getValue("type"));
239                    currentResult.lat = Double.parseDouble(atts.getValue("lat"));
240                    currentResult.lon = Double.parseDouble(atts.getValue("lon"));
241                    String[] bbox = atts.getValue("boundingbox").split(",");
242                    currentResult.bounds = new Bounds(
243                            Double.parseDouble(bbox[0]), Double.parseDouble(bbox[2]),
244                            Double.parseDouble(bbox[1]), Double.parseDouble(bbox[3]));
245                    data.add(currentResult);
246                }
247            } catch (NumberFormatException x) {
248                x.printStackTrace(); // SAXException does not chain correctly
249                throw new SAXException(x.getMessage(), x);
250            } catch (NullPointerException x) {
251                x.printStackTrace(); // SAXException does not chain correctly
252                throw new SAXException(tr("Null pointer exception, possibly some missing tags."), x);
253            }
254        }
255
256        /**
257         * Detect ending elements.
258         */
259        @Override
260        public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
261            if (qName.equals("description") && description != null) {
262                currentResult.description = description.toString();
263                description = null;
264            }
265            depth--;
266        }
267
268        /**
269         * Read characters for description.
270         */
271        @Override
272        public void characters(char[] data, int start, int length) throws org.xml.sax.SAXException {
273            if (description != null) {
274                description.append(data, start, length);
275            }
276        }
277
278        public List<SearchResult> getResult() {
279            return data;
280        }
281    }
282
283    class SearchAction extends AbstractAction implements DocumentListener {
284
285        public SearchAction() {
286            putValue(NAME, tr("Search ..."));
287            putValue(SMALL_ICON, ImageProvider.get("dialogs","search"));
288            putValue(SHORT_DESCRIPTION, tr("Click to start searching for places"));
289            updateEnabledState();
290        }
291
292        @Override
293        public void actionPerformed(ActionEvent e) {
294            if (!isEnabled() || cbSearchExpression.getText().trim().length() == 0)
295                return;
296            cbSearchExpression.addCurrentItemToHistory();
297            Main.pref.putCollection(HISTORY_KEY, cbSearchExpression.getHistory());
298            NameQueryTask task = new NameQueryTask(cbSearchExpression.getText());
299            Main.worker.submit(task);
300        }
301
302        protected void updateEnabledState() {
303            setEnabled(cbSearchExpression.getText().trim().length() > 0);
304        }
305
306        @Override
307        public void changedUpdate(DocumentEvent e) {
308            updateEnabledState();
309        }
310
311        @Override
312        public void insertUpdate(DocumentEvent e) {
313            updateEnabledState();
314        }
315
316        @Override
317        public void removeUpdate(DocumentEvent e) {
318            updateEnabledState();
319        }
320    }
321
322    class NameQueryTask extends PleaseWaitRunnable {
323
324        private String searchExpression;
325        private HttpURLConnection connection;
326        private List<SearchResult> data;
327        private boolean canceled = false;
328        private Server useserver;
329        private Exception lastException;
330
331        public NameQueryTask(String searchExpression) {
332            super(tr("Querying name server"),false /* don't ignore exceptions */);
333            this.searchExpression = searchExpression;
334            useserver = (Server)server.getSelectedItem();
335            Main.pref.put("namefinder.server", useserver.name);
336        }
337
338        @Override
339        protected void cancel() {
340            this.canceled = true;
341            synchronized (this) {
342                if (connection != null) {
343                    connection.disconnect();
344                }
345            }
346        }
347
348        @Override
349        protected void finish() {
350            if (canceled)
351                return;
352            if (lastException != null) {
353                ExceptionDialogUtil.explainException(lastException);
354                return;
355            }
356            columnmodel.setHeadlines(useserver.thirdcol, useserver.fourthcol);
357            model.setData(this.data);
358        }
359
360        @Override
361        protected void realRun() throws SAXException, IOException, OsmTransferException {
362            String urlString = useserver.url+java.net.URLEncoder.encode(searchExpression, "UTF-8");
363
364            try {
365                getProgressMonitor().indeterminateSubTask(tr("Querying name server ..."));
366                URL url = new URL(urlString);
367                synchronized(this) {
368                    connection = Utils.openHttpConnection(url);
369                }
370                connection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
371                InputStream inputStream = connection.getInputStream();
372                InputSource inputSource = new InputSource(new InputStreamReader(inputStream, "UTF-8"));
373                NameFinderResultParser parser = new NameFinderResultParser();
374                SAXParserFactory.newInstance().newSAXParser().parse(inputSource, parser);
375                this.data = parser.getResult();
376            } catch(Exception e) {
377                if (canceled)
378                    // ignore exception
379                    return;
380                OsmTransferException ex = new OsmTransferException(e);
381                ex.setUrl(urlString);
382                lastException = ex;
383            }
384        }
385    }
386
387    static class NamedResultTableModel extends DefaultTableModel {
388        private List<SearchResult> data;
389        private ListSelectionModel selectionModel;
390
391        public NamedResultTableModel(ListSelectionModel selectionModel) {
392            data = new ArrayList<SearchResult>();
393            this.selectionModel = selectionModel;
394        }
395        @Override
396        public int getRowCount() {
397            if (data == null) return 0;
398            return data.size();
399        }
400
401        @Override
402        public Object getValueAt(int row, int column) {
403            if (data == null) return null;
404            return data.get(row);
405        }
406
407        public void setData(List<SearchResult> data) {
408            if (data == null) {
409                this.data.clear();
410            } else {
411                this.data  =new ArrayList<SearchResult>(data);
412            }
413            fireTableDataChanged();
414        }
415        @Override
416        public boolean isCellEditable(int row, int column) {
417            return false;
418        }
419
420        public SearchResult getSelectedSearchResult() {
421            if (selectionModel.getMinSelectionIndex() < 0)
422                return null;
423            return data.get(selectionModel.getMinSelectionIndex());
424        }
425    }
426
427    static class NamedResultTableColumnModel extends DefaultTableColumnModel {
428        TableColumn col3 = null;
429        TableColumn col4 = null;
430        protected void createColumns() {
431            TableColumn col = null;
432            NamedResultCellRenderer renderer = new NamedResultCellRenderer();
433
434            // column 0 - Name
435            col = new TableColumn(0);
436            col.setHeaderValue(tr("Name"));
437            col.setResizable(true);
438            col.setPreferredWidth(200);
439            col.setCellRenderer(renderer);
440            addColumn(col);
441
442            // column 1 - Version
443            col = new TableColumn(1);
444            col.setHeaderValue(tr("Type"));
445            col.setResizable(true);
446            col.setPreferredWidth(100);
447            col.setCellRenderer(renderer);
448            addColumn(col);
449
450            // column 2 - Near
451            col3 = new TableColumn(2);
452            col3.setHeaderValue(servers[0].thirdcol);
453            col3.setResizable(true);
454            col3.setPreferredWidth(100);
455            col3.setCellRenderer(renderer);
456            addColumn(col3);
457
458            // column 3 - Zoom
459            col4 = new TableColumn(3);
460            col4.setHeaderValue(servers[0].fourthcol);
461            col4.setResizable(true);
462            col4.setPreferredWidth(50);
463            col4.setCellRenderer(renderer);
464            addColumn(col4);
465        }
466        public void setHeadlines(String third, String fourth) {
467            col3.setHeaderValue(third);
468            col4.setHeaderValue(fourth);
469            fireColumnMarginChanged();
470        }
471
472        public NamedResultTableColumnModel() {
473            createColumns();
474        }
475    }
476
477    class ListSelectionHandler implements ListSelectionListener {
478        @Override
479        public void valueChanged(ListSelectionEvent lse) {
480            SearchResult r = model.getSelectedSearchResult();
481            if (r != null) {
482                parent.boundingBoxChanged(r.getDownloadArea(), PlaceSelection.this);
483            }
484        }
485    }
486
487    static class NamedResultCellRenderer extends JLabel implements TableCellRenderer {
488
489        public NamedResultCellRenderer() {
490            setOpaque(true);
491            setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
492        }
493
494        protected void reset() {
495            setText("");
496            setIcon(null);
497        }
498
499        protected void renderColor(boolean selected) {
500            if (selected) {
501                setForeground(UIManager.getColor("Table.selectionForeground"));
502                setBackground(UIManager.getColor("Table.selectionBackground"));
503            } else {
504                setForeground(UIManager.getColor("Table.foreground"));
505                setBackground(UIManager.getColor("Table.background"));
506            }
507        }
508
509        protected String lineWrapDescription(String description) {
510            StringBuffer ret = new StringBuffer();
511            StringBuffer line = new StringBuffer();
512            StringTokenizer tok = new StringTokenizer(description, " ");
513            while(tok.hasMoreElements()) {
514                String t = tok.nextToken();
515                if (line.length() == 0) {
516                    line.append(t);
517                } else if (line.length() < 80) {
518                    line.append(" ").append(t);
519                } else {
520                    line.append(" ").append(t).append("<br>");
521                    ret.append(line);
522                    line = new StringBuffer();
523                }
524            }
525            ret.insert(0, "<html>");
526            ret.append("</html>");
527            return ret.toString();
528        }
529
530        @Override
531        public Component getTableCellRendererComponent(JTable table, Object value,
532                boolean isSelected, boolean hasFocus, int row, int column) {
533
534            reset();
535            renderColor(isSelected);
536
537            if (value == null) return this;
538            SearchResult sr = (SearchResult) value;
539            switch(column) {
540            case 0:
541                setText(sr.name);
542                break;
543            case 1:
544                setText(sr.info);
545                break;
546            case 2:
547                setText(sr.nearestPlace);
548                break;
549            case 3:
550                if(sr.bounds != null) {
551                    setText(sr.bounds.toShortString(new DecimalFormat("0.000")));
552                } else {
553                    setText(sr.zoom != 0 ? Integer.toString(sr.zoom) : tr("unknown"));
554                }
555                break;
556            }
557            setToolTipText(lineWrapDescription(sr.description));
558            return this;
559        }
560    }
561}