source: josm/trunk/src/org/openstreetmap/josm/gui/download/PlaceSelection.java

Last change on this file was 19050, checked in by taylor.smock, 2 years ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 18.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.download;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.GridBagConstraints;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.MouseAdapter;
13import java.awt.event.MouseEvent;
14import java.io.IOException;
15import java.io.Reader;
16import java.net.URL;
17import java.text.DecimalFormat;
18import java.util.ArrayList;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.List;
22import java.util.Objects;
23import java.util.StringTokenizer;
24import java.util.function.BiFunction;
25import java.util.function.Consumer;
26
27import javax.swing.AbstractAction;
28import javax.swing.BorderFactory;
29import javax.swing.DefaultListSelectionModel;
30import javax.swing.JButton;
31import javax.swing.JLabel;
32import javax.swing.JOptionPane;
33import javax.swing.JPanel;
34import javax.swing.JScrollPane;
35import javax.swing.JTable;
36import javax.swing.ListSelectionModel;
37import javax.swing.UIManager;
38import javax.swing.event.DocumentEvent;
39import javax.swing.event.DocumentListener;
40import javax.swing.event.ListSelectionEvent;
41import javax.swing.event.ListSelectionListener;
42import javax.swing.table.DefaultTableColumnModel;
43import javax.swing.table.DefaultTableModel;
44import javax.swing.table.TableCellRenderer;
45import javax.swing.table.TableColumn;
46import javax.xml.parsers.ParserConfigurationException;
47
48import org.openstreetmap.josm.data.Bounds;
49import org.openstreetmap.josm.gui.ExceptionDialogUtil;
50import org.openstreetmap.josm.gui.HelpAwareOptionPane;
51import org.openstreetmap.josm.gui.MainApplication;
52import org.openstreetmap.josm.gui.PleaseWaitRunnable;
53import org.openstreetmap.josm.gui.util.GuiHelper;
54import org.openstreetmap.josm.gui.util.TableHelper;
55import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
56import org.openstreetmap.josm.gui.widgets.JosmComboBox;
57import org.openstreetmap.josm.io.NameFinder;
58import org.openstreetmap.josm.io.NameFinder.SearchResult;
59import org.openstreetmap.josm.io.OsmTransferException;
60import org.openstreetmap.josm.spi.preferences.Config;
61import org.openstreetmap.josm.tools.GBC;
62import org.openstreetmap.josm.tools.HttpClient;
63import org.openstreetmap.josm.tools.ImageProvider;
64import org.openstreetmap.josm.tools.Logging;
65import org.xml.sax.SAXException;
66import org.xml.sax.SAXParseException;
67
68/**
69 * Place selector.
70 * @since 1329
71 */
72public class PlaceSelection implements DownloadSelection {
73 private static final String HISTORY_KEY = "download.places.history";
74
75 private HistoryComboBox cbSearchExpression;
76 private NamedResultTableModel model;
77 private NamedResultTableColumnModel columnmodel;
78 private JTable tblSearchResults;
79 private DownloadDialog parent;
80 private static final Server[] SERVERS = {
81 new Server("Nominatim", NameFinder::buildNominatimURL, tr("Class Type"), tr("Bounds"))
82 };
83 private final JosmComboBox<Server> serverComboBox = new JosmComboBox<>(SERVERS);
84
85 private static class Server {
86 final String name;
87 final BiFunction<String, Collection<SearchResult>, URL> urlFunction;
88 final String thirdcol;
89 final String fourthcol;
90
91 Server(String n, BiFunction<String, Collection<SearchResult>, URL> u, String t, String f) {
92 name = n;
93 urlFunction = u;
94 thirdcol = t;
95 fourthcol = f;
96 }
97
98 @Override
99 public String toString() {
100 return name;
101 }
102 }
103
104 protected JPanel buildSearchPanel() {
105 JPanel lpanel = new JPanel(new GridBagLayout());
106 JPanel panel = new JPanel(new GridBagLayout());
107
108 lpanel.add(new JLabel(tr("Choose the server for searching:")), GBC.std(0, 0).weight(0, 0).insets(0, 0, 5, 0));
109 lpanel.add(serverComboBox, GBC.std(1, 0).fill(GridBagConstraints.HORIZONTAL));
110 String s = Config.getPref().get("namefinder.server", SERVERS[0].name);
111 for (int i = 0; i < SERVERS.length; ++i) {
112 if (SERVERS[i].name.equals(s)) {
113 serverComboBox.setSelectedIndex(i);
114 }
115 }
116 lpanel.add(new JLabel(tr("Enter a place name to search for:")), GBC.std(0, 1).weight(0, 0).insets(0, 0, 5, 0));
117
118 cbSearchExpression = new HistoryComboBox();
119 cbSearchExpression.setToolTipText(tr("Enter a place name to search for"));
120 cbSearchExpression.getModel().prefs().load(HISTORY_KEY);
121 lpanel.add(cbSearchExpression, GBC.std(1, 1).fill(GridBagConstraints.HORIZONTAL));
122
123 panel.add(lpanel, GBC.std().fill(GridBagConstraints.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 * <p>
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 TableHelper.setFont(tblSearchResults, DownloadDialog.class);
149 tblSearchResults.setSelectionModel(selectionModel);
150 JScrollPane scrollPane = new JScrollPane(tblSearchResults);
151 scrollPane.setPreferredSize(new Dimension(200, 200));
152 panel.add(scrollPane, BorderLayout.CENTER);
153
154 if (gui != null)
155 gui.addDownloadAreaSelector(panel, tr("Areas around places"));
156
157 scrollPane.setPreferredSize(scrollPane.getPreferredSize());
158 tblSearchResults.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
159 tblSearchResults.getSelectionModel().addListSelectionListener(new ListSelectionHandler());
160 tblSearchResults.addMouseListener(new MouseAdapter() {
161 @Override
162 public void mouseClicked(MouseEvent e) {
163 if (e.getClickCount() > 1) {
164 SearchResult sr = model.getSelectedSearchResult();
165 if (sr != null) {
166 parent.startDownload(sr.getDownloadArea());
167 }
168 }
169 }
170 });
171 parent = gui;
172 }
173
174 @Override
175 public void setDownloadArea(Bounds area) {
176 tblSearchResults.clearSelection();
177 }
178
179 /**
180 * Action to perform initial search, and (if query is unchanged) load more results.
181 */
182 class SearchAction extends AbstractAction implements DocumentListener {
183
184 String lastSearchExpression;
185 boolean isSearchMore;
186
187 SearchAction() {
188 new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
189 updateState();
190 }
191
192 @Override
193 public void actionPerformed(ActionEvent e) {
194 String searchExpression = cbSearchExpression.getText();
195 if (!isEnabled() || searchExpression.trim().isEmpty() || serverComboBox.getSelectedItem() == null)
196 return;
197 cbSearchExpression.addCurrentItemToHistory();
198 cbSearchExpression.getModel().prefs().save(HISTORY_KEY);
199 Server server = (Server) serverComboBox.getSelectedItem();
200 URL url = server.urlFunction.apply(searchExpression, isSearchMore ? model.getData() : Collections.emptyList());
201 NameQueryTask task = new NameQueryTask(url, data -> {
202 if (isSearchMore) {
203 model.addData(data);
204 } else {
205 model.setData(data);
206 }
207 Config.getPref().put("namefinder.server", server.name);
208 columnmodel.setHeadlines(server.thirdcol, server.fourthcol);
209 lastSearchExpression = searchExpression;
210 updateState();
211 });
212 MainApplication.worker.submit(task);
213 }
214
215 protected final void updateState() {
216 String searchExpression = cbSearchExpression.getText();
217 setEnabled(!searchExpression.trim().isEmpty());
218 isSearchMore = Objects.equals(lastSearchExpression, searchExpression) && !model.getData().isEmpty();
219 if (isSearchMore) {
220 putValue(NAME, tr("Search more..."));
221 putValue(SHORT_DESCRIPTION, tr("Click to search for more places"));
222 } else {
223 putValue(NAME, tr("Search..."));
224 putValue(SHORT_DESCRIPTION, tr("Click to start searching for places"));
225 }
226 }
227
228 @Override
229 public void changedUpdate(DocumentEvent e) {
230 updateState();
231 }
232
233 @Override
234 public void insertUpdate(DocumentEvent e) {
235 updateState();
236 }
237
238 @Override
239 public void removeUpdate(DocumentEvent e) {
240 updateState();
241 }
242 }
243
244 static class NameQueryTask extends PleaseWaitRunnable {
245
246 private final URL url;
247 private final Consumer<List<SearchResult>> dataConsumer;
248 private HttpClient connection;
249 private List<SearchResult> data;
250 private boolean canceled;
251 private Exception lastException;
252
253 NameQueryTask(URL url, Consumer<List<SearchResult>> dataConsumer) {
254 super(tr("Querying name server"), false /* don't ignore exceptions */);
255 this.url = url;
256 this.dataConsumer = dataConsumer;
257 }
258
259 @Override
260 protected void cancel() {
261 this.canceled = true;
262 synchronized (this) {
263 if (connection != null) {
264 connection.disconnect();
265 }
266 }
267 }
268
269 @Override
270 protected void finish() {
271 if (canceled)
272 return;
273 if (lastException != null) {
274 ExceptionDialogUtil.explainException(lastException);
275 return;
276 }
277 dataConsumer.accept(data);
278 }
279
280 @Override
281 protected void realRun() throws SAXException, IOException, OsmTransferException {
282 try {
283 getProgressMonitor().indeterminateSubTask(tr("Querying name server ..."));
284 synchronized (this) {
285 connection = HttpClient.create(url);
286 connection.connect();
287 }
288 try (Reader reader = connection.getResponse().getContentReader()) {
289 data = NameFinder.parseSearchResults(reader);
290 }
291 } catch (SAXParseException e) {
292 if (!canceled) {
293 // Nominatim sometimes returns garbage, see #5934, #10643
294 Logging.log(Logging.LEVEL_WARN, tr("Error occurred with query ''{0}'': ''{1}''", url, e.getMessage()), e);
295 GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog(
296 MainApplication.getMainFrame(),
297 tr("Name server returned invalid data. Please try again."),
298 tr("Bad response"),
299 JOptionPane.WARNING_MESSAGE, null
300 ));
301 generateLastException(e);
302 }
303 } catch (IOException | ParserConfigurationException e) {
304 generateLastException(e);
305 }
306 }
307
308 /**
309 * Generate an {@link OsmTransferException} that will be stored in {@link #lastException} if the operation is
310 * not cancelled.
311 * @param throwable The throwable to store as an {@link OsmTransferException}
312 */
313 private void generateLastException(Throwable throwable) {
314 if (!canceled) {
315 OsmTransferException ex = new OsmTransferException(throwable);
316 ex.setUrl(url.toString());
317 lastException = ex;
318 }
319 }
320 }
321
322 static class NamedResultTableModel extends DefaultTableModel {
323 private transient List<SearchResult> data;
324 private final transient ListSelectionModel selectionModel;
325
326 NamedResultTableModel(ListSelectionModel selectionModel) {
327 data = new ArrayList<>();
328 this.selectionModel = selectionModel;
329 }
330
331 @Override
332 public int getRowCount() {
333 return data != null ? data.size() : 0;
334 }
335
336 @Override
337 public Object getValueAt(int row, int column) {
338 return data != null ? data.get(row) : null;
339 }
340
341 public void setData(List<SearchResult> data) {
342 if (data == null) {
343 this.data.clear();
344 } else {
345 this.data = new ArrayList<>(data);
346 }
347 fireTableDataChanged();
348 }
349
350 /**
351 * Add data to the table
352 * @param data The data to add
353 */
354 public void addData(List<SearchResult> data) {
355 this.data.addAll(data);
356 fireTableDataChanged();
357 }
358
359 public List<SearchResult> getData() {
360 return Collections.unmodifiableList(data);
361 }
362
363 @Override
364 public boolean isCellEditable(int row, int column) {
365 return false;
366 }
367
368 public SearchResult getSelectedSearchResult() {
369 if (selectionModel.getMinSelectionIndex() < 0)
370 return null;
371 return data.get(selectionModel.getMinSelectionIndex());
372 }
373 }
374
375 static class NamedResultTableColumnModel extends DefaultTableColumnModel {
376 private TableColumn col3;
377 private TableColumn col4;
378
379 NamedResultTableColumnModel() {
380 createColumns();
381 }
382
383 protected final void createColumns() {
384 TableColumn col;
385 NamedResultCellRenderer renderer = new NamedResultCellRenderer();
386
387 // column 0 - Name
388 col = new TableColumn(0);
389 col.setHeaderValue(tr("Name"));
390 col.setResizable(true);
391 col.setPreferredWidth(200);
392 col.setCellRenderer(renderer);
393 addColumn(col);
394
395 // column 1 - Version
396 col = new TableColumn(1);
397 col.setHeaderValue(tr("Type"));
398 col.setResizable(true);
399 col.setPreferredWidth(100);
400 col.setCellRenderer(renderer);
401 addColumn(col);
402
403 // column 2 - Near
404 col3 = new TableColumn(2);
405 col3.setHeaderValue(SERVERS[0].thirdcol);
406 col3.setResizable(true);
407 col3.setPreferredWidth(100);
408 col3.setCellRenderer(renderer);
409 addColumn(col3);
410
411 // column 3 - Zoom
412 col4 = new TableColumn(3);
413 col4.setHeaderValue(SERVERS[0].fourthcol);
414 col4.setResizable(true);
415 col4.setPreferredWidth(50);
416 col4.setCellRenderer(renderer);
417 addColumn(col4);
418 }
419
420 /**
421 * Set the header column values for the third and fourth columns
422 * @param third The new header for the third column
423 * @param fourth The new header for the fourth column
424 */
425 public void setHeadlines(String third, String fourth) {
426 col3.setHeaderValue(third);
427 col4.setHeaderValue(fourth);
428 fireColumnMarginChanged();
429 }
430 }
431
432 class ListSelectionHandler implements ListSelectionListener {
433 @Override
434 public void valueChanged(ListSelectionEvent lse) {
435 SearchResult r = model.getSelectedSearchResult();
436 if (r != null) {
437 parent.boundingBoxChanged(r.getDownloadArea(), PlaceSelection.this);
438 }
439 }
440 }
441
442 static class NamedResultCellRenderer extends JLabel implements TableCellRenderer {
443
444 /**
445 * Constructs a new {@code NamedResultCellRenderer}.
446 */
447 NamedResultCellRenderer() {
448 setOpaque(true);
449 setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
450 }
451
452 protected void reset() {
453 setText("");
454 setIcon(null);
455 }
456
457 protected void renderColor(boolean selected) {
458 if (selected) {
459 setForeground(UIManager.getColor("Table.selectionForeground"));
460 setBackground(UIManager.getColor("Table.selectionBackground"));
461 } else {
462 setForeground(UIManager.getColor("Table.foreground"));
463 setBackground(UIManager.getColor("Table.background"));
464 }
465 }
466
467 protected String lineWrapDescription(String description) {
468 StringBuilder ret = new StringBuilder();
469 StringBuilder line = new StringBuilder();
470 StringTokenizer tok = new StringTokenizer(description, " ");
471 while (tok.hasMoreElements()) {
472 String t = tok.nextToken();
473 if (line.length() == 0) {
474 line.append(t);
475 } else if (line.length() < 80) {
476 line.append(' ').append(t);
477 } else {
478 line.append(' ').append(t).append("<br>");
479 ret.append(line);
480 line = new StringBuilder();
481 }
482 }
483 ret.insert(0, "<html>");
484 ret.append("</html>");
485 return ret.toString();
486 }
487
488 @Override
489 public Component getTableCellRendererComponent(JTable table, Object value,
490 boolean isSelected, boolean hasFocus, int row, int column) {
491
492 reset();
493 renderColor(isSelected);
494
495 if (value == null)
496 return this;
497 SearchResult sr = (SearchResult) value;
498 switch (column) {
499 case 0:
500 setText(sr.getName());
501 break;
502 case 1:
503 setText(sr.getInfo());
504 break;
505 case 2:
506 setText(sr.getNearestPlace());
507 break;
508 case 3:
509 if (sr.getBounds() != null) {
510 setText(sr.getBounds().toShortString(new DecimalFormat("0.000")));
511 } else {
512 setText(sr.getZoom() != 0 ? Integer.toString(sr.getZoom()) : tr("unknown"));
513 }
514 break;
515 default: // Do nothing
516 }
517 setToolTipText(lineWrapDescription(sr.getDescription()));
518 return this;
519 }
520 }
521}
Note: See TracBrowser for help on using the repository browser.