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

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

see #12292 - Allow to disconnect HttpClient in connecting phase

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