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