001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.download;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.BorderLayout;
007    import java.awt.Component;
008    import java.awt.Dimension;
009    import java.awt.GridBagLayout;
010    import java.awt.GridLayout;
011    import java.awt.event.ActionEvent;
012    import java.awt.event.MouseAdapter;
013    import java.awt.event.MouseEvent;
014    import java.io.IOException;
015    import java.io.InputStream;
016    import java.io.InputStreamReader;
017    import java.net.HttpURLConnection;
018    import java.net.URL;
019    import java.text.DecimalFormat;
020    import java.util.ArrayList;
021    import java.util.Collections;
022    import java.util.LinkedList;
023    import java.util.List;
024    import java.util.StringTokenizer;
025    
026    import javax.swing.AbstractAction;
027    import javax.swing.BorderFactory;
028    import javax.swing.DefaultListSelectionModel;
029    import javax.swing.JButton;
030    import javax.swing.JLabel;
031    import javax.swing.JPanel;
032    import javax.swing.JScrollPane;
033    import javax.swing.JTable;
034    import javax.swing.JTextField;
035    import javax.swing.ListSelectionModel;
036    import javax.swing.UIManager;
037    import javax.swing.event.DocumentEvent;
038    import javax.swing.event.DocumentListener;
039    import javax.swing.event.ListSelectionEvent;
040    import javax.swing.event.ListSelectionListener;
041    import javax.swing.table.DefaultTableColumnModel;
042    import javax.swing.table.DefaultTableModel;
043    import javax.swing.table.TableCellRenderer;
044    import javax.swing.table.TableColumn;
045    import javax.xml.parsers.SAXParserFactory;
046    
047    import org.openstreetmap.josm.Main;
048    import org.openstreetmap.josm.data.Bounds;
049    import org.openstreetmap.josm.data.coor.LatLon;
050    import org.openstreetmap.josm.gui.ExceptionDialogUtil;
051    import org.openstreetmap.josm.gui.PleaseWaitRunnable;
052    import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
053    import org.openstreetmap.josm.gui.widgets.JosmComboBox;
054    import org.openstreetmap.josm.io.OsmTransferException;
055    import org.openstreetmap.josm.tools.GBC;
056    import org.openstreetmap.josm.tools.ImageProvider;
057    import org.openstreetmap.josm.tools.OsmUrlToBounds;
058    import org.xml.sax.Attributes;
059    import org.xml.sax.InputSource;
060    import org.xml.sax.SAXException;
061    import org.xml.sax.helpers.DefaultHandler;
062    
063    public 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        public void addGui(final DownloadDialog gui) {
135            JPanel panel = new JPanel();
136            panel.setLayout(new BorderLayout());
137            panel.add(buildSearchPanel(), BorderLayout.NORTH);
138    
139            DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
140            model = new NamedResultTableModel(selectionModel);
141            columnmodel = new NamedResultTableColumnModel();
142            tblSearchResults = new JTable(model, columnmodel);
143            tblSearchResults.setSelectionModel(selectionModel);
144            JScrollPane scrollPane = new JScrollPane(tblSearchResults);
145            scrollPane.setPreferredSize(new Dimension(200,200));
146            panel.add(scrollPane, BorderLayout.CENTER);
147    
148            gui.addDownloadAreaSelector(panel, tr("Areas around places"));
149    
150            scrollPane.setPreferredSize(scrollPane.getPreferredSize());
151            tblSearchResults.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
152            tblSearchResults.getSelectionModel().addListSelectionListener(new ListSelectionHandler());
153            tblSearchResults.addMouseListener(new MouseAdapter() {
154                @Override public void mouseClicked(MouseEvent e) {
155                    if (e.getClickCount() > 1) {
156                        SearchResult sr = model.getSelectedSearchResult();
157                        if (sr == null) return;
158                        parent.startDownload(sr.getDownloadArea());
159                    }
160                }
161            });
162            parent = gui;
163        }
164    
165        public void setDownloadArea(Bounds area) {
166            tblSearchResults.clearSelection();
167        }
168    
169        /**
170         * Data storage for search results.
171         */
172        static private class SearchResult {
173            public String name;
174            public String info;
175            public String nearestPlace;
176            public String description;
177            public double lat;
178            public double lon;
179            public int zoom = 0;
180            public Bounds bounds = null;
181    
182            public Bounds getDownloadArea() {
183                return bounds != null ? bounds : OsmUrlToBounds.positionToBounds(lat, lon, zoom);
184            }
185        }
186    
187        /**
188         * A very primitive parser for the name finder's output.
189         * Structure of xml described here:  http://wiki.openstreetmap.org/index.php/Name_finder
190         *
191         */
192        private static class NameFinderResultParser extends DefaultHandler {
193            private SearchResult currentResult = null;
194            private StringBuffer description = null;
195            private int depth = 0;
196            private List<SearchResult> data = new LinkedList<SearchResult>();
197    
198            /**
199             * Detect starting elements.
200             *
201             */
202            @Override
203            public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
204            throws SAXException {
205                depth++;
206                try {
207                    if (qName.equals("searchresults")) {
208                        // do nothing
209                    } else if (qName.equals("named") && (depth == 2)) {
210                        currentResult = new PlaceSelection.SearchResult();
211                        currentResult.name = atts.getValue("name");
212                        currentResult.info = atts.getValue("info");
213                        if(currentResult.info != null) {
214                            currentResult.info = tr(currentResult.info);
215                        }
216                        currentResult.lat = Double.parseDouble(atts.getValue("lat"));
217                        currentResult.lon = Double.parseDouble(atts.getValue("lon"));
218                        currentResult.zoom = Integer.parseInt(atts.getValue("zoom"));
219                        data.add(currentResult);
220                    } else if (qName.equals("description") && (depth == 3)) {
221                        description = new StringBuffer();
222                    } else if (qName.equals("named") && (depth == 4)) {
223                        // this is a "named" place in the nearest places list.
224                        String info = atts.getValue("info");
225                        if ("city".equals(info) || "town".equals(info) || "village".equals(info)) {
226                            currentResult.nearestPlace = atts.getValue("name");
227                        }
228                    } else if (qName.equals("place") && atts.getValue("lat") != null) {
229                        currentResult = new PlaceSelection.SearchResult();
230                        currentResult.name = atts.getValue("display_name");
231                        currentResult.description = currentResult.name;
232                        currentResult.info = atts.getValue("class");
233                        if (currentResult.info != null) {
234                            currentResult.info = tr(currentResult.info);
235                        }
236                        currentResult.nearestPlace = tr(atts.getValue("type"));
237                        currentResult.lat = Double.parseDouble(atts.getValue("lat"));
238                        currentResult.lon = Double.parseDouble(atts.getValue("lon"));
239                        String[] bbox = atts.getValue("boundingbox").split(",");
240                        currentResult.bounds = new Bounds(
241                                new LatLon(Double.parseDouble(bbox[0]), Double.parseDouble(bbox[2])),
242                                new LatLon(Double.parseDouble(bbox[1]), Double.parseDouble(bbox[3])));
243                        data.add(currentResult);
244                    }
245                } catch (NumberFormatException x) {
246                    x.printStackTrace(); // SAXException does not chain correctly
247                    throw new SAXException(x.getMessage(), x);
248                } catch (NullPointerException x) {
249                    x.printStackTrace(); // SAXException does not chain correctly
250                    throw new SAXException(tr("Null pointer exception, possibly some missing tags."), x);
251                }
252            }
253    
254            /**
255             * Detect ending elements.
256             */
257            @Override
258            public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
259                if (qName.equals("searchresults")) {
260                } else if (qName.equals("description") && description != null) {
261                    currentResult.description = description.toString();
262                    description = null;
263                }
264                depth--;
265    
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            public void actionPerformed(ActionEvent e) {
293                if (!isEnabled() || cbSearchExpression.getText().trim().length() == 0)
294                    return;
295                cbSearchExpression.addCurrentItemToHistory();
296                Main.pref.putCollection(HISTORY_KEY, cbSearchExpression.getHistory());
297                NameQueryTask task = new NameQueryTask(cbSearchExpression.getText());
298                Main.worker.submit(task);
299            }
300    
301            protected void updateEnabledState() {
302                setEnabled(cbSearchExpression.getText().trim().length() > 0);
303            }
304    
305            public void changedUpdate(DocumentEvent e) {
306                updateEnabledState();
307            }
308    
309            public void insertUpdate(DocumentEvent e) {
310                updateEnabledState();
311            }
312    
313            public void removeUpdate(DocumentEvent e) {
314                updateEnabledState();
315            }
316        }
317    
318        class NameQueryTask extends PleaseWaitRunnable {
319    
320            private String searchExpression;
321            private HttpURLConnection connection;
322            private List<SearchResult> data;
323            private boolean canceled = false;
324            private Server useserver;
325            private Exception lastException;
326    
327            public NameQueryTask(String searchExpression) {
328                super(tr("Querying name server"),false /* don't ignore exceptions */);
329                this.searchExpression = searchExpression;
330                useserver = (Server)server.getSelectedItem();
331                Main.pref.put("namefinder.server", useserver.name);
332            }
333    
334            @Override
335            protected void cancel() {
336                this.canceled = true;
337                synchronized (this) {
338                    if (connection != null) {
339                        connection.disconnect();
340                    }
341                }
342            }
343    
344            @Override
345            protected void finish() {
346                if (canceled)
347                    return;
348                if (lastException != null) {
349                    ExceptionDialogUtil.explainException(lastException);
350                    return;
351                }
352                columnmodel.setHeadlines(useserver.thirdcol, useserver.fourthcol);
353                model.setData(this.data);
354            }
355    
356            @Override
357            protected void realRun() throws SAXException, IOException, OsmTransferException {
358                String urlString = useserver.url+java.net.URLEncoder.encode(searchExpression, "UTF-8");
359    
360                try {
361                    getProgressMonitor().indeterminateSubTask(tr("Querying name server ..."));
362                    URL url = new URL(urlString);
363                    synchronized(this) {
364                        connection = (HttpURLConnection)url.openConnection();
365                    }
366                    connection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
367                    InputStream inputStream = connection.getInputStream();
368                    InputSource inputSource = new InputSource(new InputStreamReader(inputStream, "UTF-8"));
369                    NameFinderResultParser parser = new NameFinderResultParser();
370                    SAXParserFactory.newInstance().newSAXParser().parse(inputSource, parser);
371                    this.data = parser.getResult();
372                } catch(Exception e) {
373                    if (canceled)
374                        // ignore exception
375                        return;
376                    OsmTransferException ex = new OsmTransferException(e);
377                    ex.setUrl(urlString);
378                    lastException = ex;
379                }
380            }
381        }
382    
383        static class NamedResultTableModel extends DefaultTableModel {
384            private ArrayList<SearchResult> data;
385            private ListSelectionModel selectionModel;
386    
387            public NamedResultTableModel(ListSelectionModel selectionModel) {
388                data = new ArrayList<SearchResult>();
389                this.selectionModel = selectionModel;
390            }
391            @Override
392            public int getRowCount() {
393                if (data == null) return 0;
394                return data.size();
395            }
396    
397            @Override
398            public Object getValueAt(int row, int column) {
399                if (data == null) return null;
400                return data.get(row);
401            }
402    
403            public void setData(List<SearchResult> data) {
404                if (data == null) {
405                    this.data.clear();
406                } else {
407                    this.data  =new ArrayList<SearchResult>(data);
408                }
409                fireTableDataChanged();
410            }
411            @Override
412            public boolean isCellEditable(int row, int column) {
413                return false;
414            }
415    
416            public SearchResult getSelectedSearchResult() {
417                if (selectionModel.getMinSelectionIndex() < 0)
418                    return null;
419                return data.get(selectionModel.getMinSelectionIndex());
420            }
421        }
422    
423        static class NamedResultTableColumnModel extends DefaultTableColumnModel {
424            TableColumn col3 = null;
425            TableColumn col4 = null;
426            protected void createColumns() {
427                TableColumn col = null;
428                NamedResultCellRenderer renderer = new NamedResultCellRenderer();
429    
430                // column 0 - Name
431                col = new TableColumn(0);
432                col.setHeaderValue(tr("Name"));
433                col.setResizable(true);
434                col.setPreferredWidth(200);
435                col.setCellRenderer(renderer);
436                addColumn(col);
437    
438                // column 1 - Version
439                col = new TableColumn(1);
440                col.setHeaderValue(tr("Type"));
441                col.setResizable(true);
442                col.setPreferredWidth(100);
443                col.setCellRenderer(renderer);
444                addColumn(col);
445    
446                // column 2 - Near
447                col3 = new TableColumn(2);
448                col3.setHeaderValue(servers[0].thirdcol);
449                col3.setResizable(true);
450                col3.setPreferredWidth(100);
451                col3.setCellRenderer(renderer);
452                addColumn(col3);
453    
454                // column 3 - Zoom
455                col4 = new TableColumn(3);
456                col4.setHeaderValue(servers[0].fourthcol);
457                col4.setResizable(true);
458                col4.setPreferredWidth(50);
459                col4.setCellRenderer(renderer);
460                addColumn(col4);
461            }
462            public void setHeadlines(String third, String fourth) {
463                col3.setHeaderValue(third);
464                col4.setHeaderValue(fourth);
465                fireColumnMarginChanged();
466            }
467    
468            public NamedResultTableColumnModel() {
469                createColumns();
470            }
471        }
472    
473        class ListSelectionHandler implements ListSelectionListener {
474            public void valueChanged(ListSelectionEvent lse) {
475                SearchResult r = model.getSelectedSearchResult();
476                if (r != null) {
477                    parent.boundingBoxChanged(r.getDownloadArea(), PlaceSelection.this);
478                }
479            }
480        }
481    
482        static class NamedResultCellRenderer extends JLabel implements TableCellRenderer {
483    
484            public NamedResultCellRenderer() {
485                setOpaque(true);
486                setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
487            }
488    
489            protected void reset() {
490                setText("");
491                setIcon(null);
492            }
493    
494            protected void renderColor(boolean selected) {
495                if (selected) {
496                    setForeground(UIManager.getColor("Table.selectionForeground"));
497                    setBackground(UIManager.getColor("Table.selectionBackground"));
498                } else {
499                    setForeground(UIManager.getColor("Table.foreground"));
500                    setBackground(UIManager.getColor("Table.background"));
501                }
502            }
503    
504            protected String lineWrapDescription(String description) {
505                StringBuffer ret = new StringBuffer();
506                StringBuffer line = new StringBuffer();
507                StringTokenizer tok = new StringTokenizer(description, " ");
508                while(tok.hasMoreElements()) {
509                    String t = tok.nextToken();
510                    if (line.length() == 0) {
511                        line.append(t);
512                    } else if (line.length() < 80) {
513                        line.append(" ").append(t);
514                    } else {
515                        line.append(" ").append(t).append("<br>");
516                        ret.append(line);
517                        line = new StringBuffer();
518                    }
519                }
520                ret.insert(0, "<html>");
521                ret.append("</html>");
522                return ret.toString();
523            }
524    
525            public Component getTableCellRendererComponent(JTable table, Object value,
526                    boolean isSelected, boolean hasFocus, int row, int column) {
527    
528                reset();
529                renderColor(isSelected);
530    
531                if (value == null) return this;
532                SearchResult sr = (SearchResult) value;
533                switch(column) {
534                case 0:
535                    setText(sr.name);
536                    break;
537                case 1:
538                    setText(sr.info);
539                    break;
540                case 2:
541                    setText(sr.nearestPlace);
542                    break;
543                case 3:
544                    if(sr.bounds != null) {
545                        setText(sr.bounds.toShortString(new DecimalFormat("0.000")));
546                    } else {
547                        setText(sr.zoom != 0 ? Integer.toString(sr.zoom) : tr("unknown"));
548                    }
549                    break;
550                }
551                setToolTipText(lineWrapDescription(sr.description));
552                return this;
553            }
554        }
555    }