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 }