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

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

sonar - squid:S3052 - Fields should not be initialized to default values

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