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

Last change on this file since 8308 was 8308, checked in by Don-vip, 9 years ago

fix potential NPEs and Sonar issues related to serialization

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