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

Last change on this file since 9484 was 9484, checked in by simon04, 8 years ago

Refactoring: add JosmComboBox.getEditorComponent, Javadoc

  • Property svn:eol-style set to native
File size: 21.3 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.GridLayout;
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.Collections;
20import java.util.LinkedList;
21import java.util.List;
22import java.util.StringTokenizer;
23
24import javax.swing.AbstractAction;
25import javax.swing.BorderFactory;
26import javax.swing.DefaultListSelectionModel;
27import javax.swing.JButton;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JScrollPane;
32import javax.swing.JTable;
33import javax.swing.ListSelectionModel;
34import javax.swing.UIManager;
35import javax.swing.event.DocumentEvent;
36import javax.swing.event.DocumentListener;
37import javax.swing.event.ListSelectionEvent;
38import javax.swing.event.ListSelectionListener;
39import javax.swing.table.DefaultTableColumnModel;
40import javax.swing.table.DefaultTableModel;
41import javax.swing.table.TableCellRenderer;
42import javax.swing.table.TableColumn;
43
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.data.Bounds;
46import org.openstreetmap.josm.gui.ExceptionDialogUtil;
47import org.openstreetmap.josm.gui.HelpAwareOptionPane;
48import org.openstreetmap.josm.gui.PleaseWaitRunnable;
49import org.openstreetmap.josm.gui.util.GuiHelper;
50import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
51import org.openstreetmap.josm.gui.widgets.JosmComboBox;
52import org.openstreetmap.josm.io.OsmTransferException;
53import org.openstreetmap.josm.tools.GBC;
54import org.openstreetmap.josm.tools.HttpClient;
55import org.openstreetmap.josm.tools.ImageProvider;
56import org.openstreetmap.josm.tools.OsmUrlToBounds;
57import org.openstreetmap.josm.tools.Utils;
58import org.xml.sax.Attributes;
59import org.xml.sax.InputSource;
60import org.xml.sax.SAXException;
61import org.xml.sax.SAXParseException;
62import org.xml.sax.helpers.DefaultHandler;
63
64public class PlaceSelection implements DownloadSelection {
65 private static final String HISTORY_KEY = "download.places.history";
66
67 private HistoryComboBox cbSearchExpression;
68 private NamedResultTableModel model;
69 private NamedResultTableColumnModel columnmodel;
70 private JTable tblSearchResults;
71 private DownloadDialog parent;
72 private static final Server[] SERVERS = new Server[] {
73 new Server("Nominatim", "https://nominatim.openstreetmap.org/search?format=xml&q=", tr("Class Type"), tr("Bounds"))
74 };
75 private final JosmComboBox<Server> server = new JosmComboBox<>(SERVERS);
76
77 private static class Server {
78 public final String name;
79 public final String url;
80 public final String thirdcol;
81 public final String fourthcol;
82
83 Server(String n, String u, String t, String f) {
84 name = n;
85 url = u;
86 thirdcol = t;
87 fourthcol = f;
88 }
89
90 @Override
91 public String toString() {
92 return name;
93 }
94 }
95
96 protected JPanel buildSearchPanel() {
97 JPanel lpanel = new JPanel();
98 lpanel.setLayout(new GridLayout(2, 2));
99 JPanel panel = new JPanel();
100 panel.setLayout(new GridBagLayout());
101
102 lpanel.add(new JLabel(tr("Choose the server for searching:")));
103 lpanel.add(server);
104 String s = Main.pref.get("namefinder.server", SERVERS[0].name);
105 for (int i = 0; i < SERVERS.length; ++i) {
106 if (SERVERS[i].name.equals(s)) {
107 server.setSelectedIndex(i);
108 }
109 }
110 lpanel.add(new JLabel(tr("Enter a place name to search for:")));
111
112 cbSearchExpression = new HistoryComboBox();
113 cbSearchExpression.setToolTipText(tr("Enter a place name to search for"));
114 List<String> cmtHistory = new LinkedList<>(Main.pref.getCollection(HISTORY_KEY, new LinkedList<String>()));
115 Collections.reverse(cmtHistory);
116 cbSearchExpression.setPossibleItems(cmtHistory);
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();
138 panel.setLayout(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 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 public void mouseClicked(MouseEvent e) {
157 if (e.getClickCount() > 1) {
158 SearchResult sr = model.getSelectedSearchResult();
159 if (sr == null) return;
160 parent.startDownload(sr.getDownloadArea());
161 }
162 }
163 });
164 parent = gui;
165 }
166
167 @Override
168 public void setDownloadArea(Bounds area) {
169 tblSearchResults.clearSelection();
170 }
171
172 /**
173 * Data storage for search results.
174 */
175 private static class SearchResult {
176 public String name;
177 public String info;
178 public String nearestPlace;
179 public String description;
180 public double lat;
181 public double lon;
182 public int zoom;
183 public Bounds bounds;
184
185 public Bounds getDownloadArea() {
186 return bounds != null ? bounds : OsmUrlToBounds.positionToBounds(lat, lon, zoom);
187 }
188 }
189
190 /**
191 * A very primitive parser for the name finder's output.
192 * Structure of xml described here: http://wiki.openstreetmap.org/index.php/Name_finder
193 *
194 */
195 private static class NameFinderResultParser extends DefaultHandler {
196 private SearchResult currentResult;
197 private StringBuilder description;
198 private int depth;
199 private final List<SearchResult> data = new LinkedList<>();
200
201 /**
202 * Detect starting elements.
203 *
204 */
205 @Override
206 public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
207 throws SAXException {
208 depth++;
209 try {
210 if ("searchresults".equals(qName)) {
211 // do nothing
212 } else if ("named".equals(qName) && (depth == 2)) {
213 currentResult = new PlaceSelection.SearchResult();
214 currentResult.name = atts.getValue("name");
215 currentResult.info = atts.getValue("info");
216 if (currentResult.info != null) {
217 currentResult.info = tr(currentResult.info);
218 }
219 currentResult.lat = Double.parseDouble(atts.getValue("lat"));
220 currentResult.lon = Double.parseDouble(atts.getValue("lon"));
221 currentResult.zoom = Integer.parseInt(atts.getValue("zoom"));
222 data.add(currentResult);
223 } else if ("description".equals(qName) && (depth == 3)) {
224 description = new StringBuilder();
225 } else if ("named".equals(qName) && (depth == 4)) {
226 // this is a "named" place in the nearest places list.
227 String info = atts.getValue("info");
228 if ("city".equals(info) || "town".equals(info) || "village".equals(info)) {
229 currentResult.nearestPlace = atts.getValue("name");
230 }
231 } else if ("place".equals(qName) && atts.getValue("lat") != null) {
232 currentResult = new PlaceSelection.SearchResult();
233 currentResult.name = atts.getValue("display_name");
234 currentResult.description = currentResult.name;
235 currentResult.info = atts.getValue("class");
236 if (currentResult.info != null) {
237 currentResult.info = tr(currentResult.info);
238 }
239 currentResult.nearestPlace = tr(atts.getValue("type"));
240 currentResult.lat = Double.parseDouble(atts.getValue("lat"));
241 currentResult.lon = Double.parseDouble(atts.getValue("lon"));
242 String[] bbox = atts.getValue("boundingbox").split(",");
243 currentResult.bounds = new Bounds(
244 Double.parseDouble(bbox[0]), Double.parseDouble(bbox[2]),
245 Double.parseDouble(bbox[1]), Double.parseDouble(bbox[3]));
246 data.add(currentResult);
247 }
248 } catch (NumberFormatException x) {
249 Main.error(x); // SAXException does not chain correctly
250 throw new SAXException(x.getMessage(), x);
251 } catch (NullPointerException x) {
252 Main.error(x); // SAXException does not chain correctly
253 throw new SAXException(tr("Null pointer exception, possibly some missing tags."), x);
254 }
255 }
256
257 /**
258 * Detect ending elements.
259 */
260 @Override
261 public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
262 if ("description".equals(qName) && description != null) {
263 currentResult.description = description.toString();
264 description = null;
265 }
266 depth--;
267 }
268
269 /**
270 * Read characters for description.
271 */
272 @Override
273 public void characters(char[] data, int start, int length) throws org.xml.sax.SAXException {
274 if (description != null) {
275 description.append(data, start, length);
276 }
277 }
278
279 public List<SearchResult> getResult() {
280 return data;
281 }
282 }
283
284 class SearchAction extends AbstractAction implements DocumentListener {
285
286 SearchAction() {
287 putValue(NAME, tr("Search ..."));
288 putValue(SMALL_ICON, ImageProvider.get("dialogs", "search"));
289 putValue(SHORT_DESCRIPTION, tr("Click to start searching for places"));
290 updateEnabledState();
291 }
292
293 @Override
294 public void actionPerformed(ActionEvent e) {
295 if (!isEnabled() || cbSearchExpression.getText().trim().isEmpty())
296 return;
297 cbSearchExpression.addCurrentItemToHistory();
298 Main.pref.putCollection(HISTORY_KEY, cbSearchExpression.getHistory());
299 NameQueryTask task = new NameQueryTask(cbSearchExpression.getText());
300 Main.worker.submit(task);
301 }
302
303 protected final void updateEnabledState() {
304 setEnabled(!cbSearchExpression.getText().trim().isEmpty());
305 }
306
307 @Override
308 public void changedUpdate(DocumentEvent e) {
309 updateEnabledState();
310 }
311
312 @Override
313 public void insertUpdate(DocumentEvent e) {
314 updateEnabledState();
315 }
316
317 @Override
318 public void removeUpdate(DocumentEvent e) {
319 updateEnabledState();
320 }
321 }
322
323 class NameQueryTask extends PleaseWaitRunnable {
324
325 private final String searchExpression;
326 private HttpClient connection;
327 private List<SearchResult> data;
328 private boolean canceled;
329 private final Server useserver;
330 private Exception lastException;
331
332 NameQueryTask(String searchExpression) {
333 super(tr("Querying name server"), false /* don't ignore exceptions */);
334 this.searchExpression = searchExpression;
335 useserver = (Server) server.getSelectedItem();
336 Main.pref.put("namefinder.server", useserver.name);
337 }
338
339 @Override
340 protected void cancel() {
341 this.canceled = true;
342 synchronized (this) {
343 if (connection != null) {
344 connection.disconnect();
345 }
346 }
347 }
348
349 @Override
350 protected void finish() {
351 if (canceled)
352 return;
353 if (lastException != null) {
354 ExceptionDialogUtil.explainException(lastException);
355 return;
356 }
357 columnmodel.setHeadlines(useserver.thirdcol, useserver.fourthcol);
358 model.setData(this.data);
359 }
360
361 @Override
362 protected void realRun() throws SAXException, IOException, OsmTransferException {
363 String urlString = useserver.url+Utils.encodeUrl(searchExpression);
364
365 try {
366 getProgressMonitor().indeterminateSubTask(tr("Querying name server ..."));
367 URL url = new URL(urlString);
368 synchronized (this) {
369 connection = HttpClient.create(url);
370 connection.connect();
371 }
372 try (Reader reader = connection.getResponse().getContentReader()) {
373 InputSource inputSource = new InputSource(reader);
374 NameFinderResultParser parser = new NameFinderResultParser();
375 Utils.parseSafeSAX(inputSource, parser);
376 this.data = parser.getResult();
377 }
378 } catch (SAXParseException e) {
379 if (!canceled) {
380 // Nominatim sometimes returns garbage, see #5934, #10643
381 Main.warn(tr("Error occured with query ''{0}'': ''{1}''", urlString, e.getMessage()));
382 GuiHelper.runInEDTAndWait(new Runnable() {
383 @Override
384 public void run() {
385 HelpAwareOptionPane.showOptionDialog(
386 Main.parent,
387 tr("Name server returned invalid data. Please try again."),
388 tr("Bad response"),
389 JOptionPane.WARNING_MESSAGE, null
390 );
391 }
392 });
393 }
394 } catch (Exception e) {
395 if (!canceled) {
396 OsmTransferException ex = new OsmTransferException(e);
397 ex.setUrl(urlString);
398 lastException = ex;
399 }
400 }
401 }
402 }
403
404 static class NamedResultTableModel extends DefaultTableModel {
405 private transient List<SearchResult> data;
406 private final transient ListSelectionModel selectionModel;
407
408 NamedResultTableModel(ListSelectionModel selectionModel) {
409 data = new ArrayList<>();
410 this.selectionModel = selectionModel;
411 }
412
413 @Override
414 public int getRowCount() {
415 if (data == null) return 0;
416 return data.size();
417 }
418
419 @Override
420 public Object getValueAt(int row, int column) {
421 if (data == null) return null;
422 return data.get(row);
423 }
424
425 public void setData(List<SearchResult> data) {
426 if (data == null) {
427 this.data.clear();
428 } else {
429 this.data = new ArrayList<>(data);
430 }
431 fireTableDataChanged();
432 }
433
434 @Override
435 public boolean isCellEditable(int row, int column) {
436 return false;
437 }
438
439 public SearchResult getSelectedSearchResult() {
440 if (selectionModel.getMinSelectionIndex() < 0)
441 return null;
442 return data.get(selectionModel.getMinSelectionIndex());
443 }
444 }
445
446 static class NamedResultTableColumnModel extends DefaultTableColumnModel {
447 private TableColumn col3;
448 private TableColumn col4;
449 protected final void createColumns() {
450 TableColumn col = null;
451 NamedResultCellRenderer renderer = new NamedResultCellRenderer();
452
453 // column 0 - Name
454 col = new TableColumn(0);
455 col.setHeaderValue(tr("Name"));
456 col.setResizable(true);
457 col.setPreferredWidth(200);
458 col.setCellRenderer(renderer);
459 addColumn(col);
460
461 // column 1 - Version
462 col = new TableColumn(1);
463 col.setHeaderValue(tr("Type"));
464 col.setResizable(true);
465 col.setPreferredWidth(100);
466 col.setCellRenderer(renderer);
467 addColumn(col);
468
469 // column 2 - Near
470 col3 = new TableColumn(2);
471 col3.setHeaderValue(SERVERS[0].thirdcol);
472 col3.setResizable(true);
473 col3.setPreferredWidth(100);
474 col3.setCellRenderer(renderer);
475 addColumn(col3);
476
477 // column 3 - Zoom
478 col4 = new TableColumn(3);
479 col4.setHeaderValue(SERVERS[0].fourthcol);
480 col4.setResizable(true);
481 col4.setPreferredWidth(50);
482 col4.setCellRenderer(renderer);
483 addColumn(col4);
484 }
485
486 public void setHeadlines(String third, String fourth) {
487 col3.setHeaderValue(third);
488 col4.setHeaderValue(fourth);
489 fireColumnMarginChanged();
490 }
491
492 NamedResultTableColumnModel() {
493 createColumns();
494 }
495 }
496
497 class ListSelectionHandler implements ListSelectionListener {
498 @Override
499 public void valueChanged(ListSelectionEvent lse) {
500 SearchResult r = model.getSelectedSearchResult();
501 if (r != null) {
502 parent.boundingBoxChanged(r.getDownloadArea(), PlaceSelection.this);
503 }
504 }
505 }
506
507 static class NamedResultCellRenderer extends JLabel implements TableCellRenderer {
508
509 /**
510 * Constructs a new {@code NamedResultCellRenderer}.
511 */
512 NamedResultCellRenderer() {
513 setOpaque(true);
514 setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
515 }
516
517 protected void reset() {
518 setText("");
519 setIcon(null);
520 }
521
522 protected void renderColor(boolean selected) {
523 if (selected) {
524 setForeground(UIManager.getColor("Table.selectionForeground"));
525 setBackground(UIManager.getColor("Table.selectionBackground"));
526 } else {
527 setForeground(UIManager.getColor("Table.foreground"));
528 setBackground(UIManager.getColor("Table.background"));
529 }
530 }
531
532 protected String lineWrapDescription(String description) {
533 StringBuilder ret = new StringBuilder();
534 StringBuilder line = new StringBuilder();
535 StringTokenizer tok = new StringTokenizer(description, " ");
536 while (tok.hasMoreElements()) {
537 String t = tok.nextToken();
538 if (line.length() == 0) {
539 line.append(t);
540 } else if (line.length() < 80) {
541 line.append(' ').append(t);
542 } else {
543 line.append(' ').append(t).append("<br>");
544 ret.append(line);
545 line = new StringBuilder();
546 }
547 }
548 ret.insert(0, "<html>");
549 ret.append("</html>");
550 return ret.toString();
551 }
552
553 @Override
554 public Component getTableCellRendererComponent(JTable table, Object value,
555 boolean isSelected, boolean hasFocus, int row, int column) {
556
557 reset();
558 renderColor(isSelected);
559
560 if (value == null) return this;
561 SearchResult sr = (SearchResult) value;
562 switch(column) {
563 case 0:
564 setText(sr.name);
565 break;
566 case 1:
567 setText(sr.info);
568 break;
569 case 2:
570 setText(sr.nearestPlace);
571 break;
572 case 3:
573 if (sr.bounds != null) {
574 setText(sr.bounds.toShortString(new DecimalFormat("0.000")));
575 } else {
576 setText(sr.zoom != 0 ? Integer.toString(sr.zoom) : tr("unknown"));
577 }
578 break;
579 }
580 setToolTipText(lineWrapDescription(sr.description));
581 return this;
582 }
583 }
584}
Note: See TracBrowser for help on using the repository browser.