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}