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