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

Last change on this file since 17318 was 16960, checked in by simon04, 4 years ago

see #8334 - Add advanced option to scale the table font

Advanced preference keys gui.scale.table.*

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