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

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

refactoring - global simplification of use of setLayout method - simply pass layout to JPanel constructor

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