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