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