source: josm/trunk/src/org/openstreetmap/josm/actions/OverpassDownloadAction.java @ 12574

Last change on this file since 12574 was 12574, checked in by michael2402, 4 months ago

Apply #15057: Improve the over pass turbo dialog

Adds the ability to add favorites and a new wizard dialog with examples.

File size: 20.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.FocusEvent;
13import java.awt.event.FocusListener;
14import java.awt.event.KeyEvent;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.Collection;
18import java.util.Collections;
19import java.util.Optional;
20import java.util.concurrent.Future;
21import java.util.function.Consumer;
22
23import javax.swing.AbstractAction;
24import javax.swing.Action;
25import javax.swing.ActionMap;
26import javax.swing.JButton;
27import javax.swing.JEditorPane;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JScrollPane;
32import javax.swing.event.HyperlinkEvent;
33import javax.swing.plaf.basic.BasicArrowButton;
34import javax.swing.text.JTextComponent;
35
36import org.openstreetmap.josm.Main;
37import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
38import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
39import org.openstreetmap.josm.data.Bounds;
40import org.openstreetmap.josm.data.preferences.BooleanProperty;
41import org.openstreetmap.josm.data.preferences.CollectionProperty;
42import org.openstreetmap.josm.gui.ExtendedDialog;
43import org.openstreetmap.josm.gui.download.DownloadDialog;
44import org.openstreetmap.josm.gui.download.OverpassQueryList;
45import org.openstreetmap.josm.gui.preferences.server.OverpassServerPreference;
46import org.openstreetmap.josm.gui.util.GuiHelper;
47import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
48import org.openstreetmap.josm.gui.widgets.JosmTextArea;
49import org.openstreetmap.josm.io.OverpassDownloadReader;
50import org.openstreetmap.josm.tools.GBC;
51import org.openstreetmap.josm.tools.OpenBrowser;
52import org.openstreetmap.josm.tools.OverpassTurboQueryWizard;
53import org.openstreetmap.josm.tools.Shortcut;
54import org.openstreetmap.josm.tools.UncheckedParseException;
55import org.openstreetmap.josm.tools.Utils;
56
57/**
58 * Download map data from Overpass API server.
59 * @since 8684
60 */
61public class OverpassDownloadAction extends JosmAction {
62
63    /**
64     * Constructs a new {@code OverpassDownloadAction}.
65     */
66    public OverpassDownloadAction() {
67        super(tr("Download from Overpass API ..."), "download-overpass", tr("Download map data from Overpass API server."),
68                // CHECKSTYLE.OFF: LineLength
69                Shortcut.registerShortcut("file:download-overpass", tr("File: {0}", tr("Download from Overpass API ...")), KeyEvent.VK_DOWN, Shortcut.ALT_SHIFT),
70                // CHECKSTYLE.ON: LineLength
71                true, "overpassdownload/download", true);
72        putValue("help", ht("/Action/OverpassDownload"));
73    }
74
75    @Override
76    public void actionPerformed(ActionEvent e) {
77        OverpassDownloadDialog dialog = OverpassDownloadDialog.getInstance();
78        dialog.restoreSettings();
79        dialog.setVisible(true);
80
81        if (dialog.isCanceled()) {
82            return;
83        }
84
85        dialog.rememberSettings();
86        Optional<Bounds> selectedArea = dialog.getSelectedDownloadArea();
87        String overpassQuery = dialog.getOverpassQuery();
88
89        /*
90         * Absence of the selected area can be justified only if the overpass query
91         * is not restricted to bbox.
92         */
93        if (!selectedArea.isPresent() && overpassQuery.contains("{{bbox}}")) {
94            JOptionPane.showMessageDialog(
95                    dialog,
96                    tr("Please select a download area first."),
97                    tr("Error"),
98                    JOptionPane.ERROR_MESSAGE
99            );
100            return;
101        }
102
103        /*
104         * A callback that is passed to PostDownloadReporter that is called once the download task
105         * has finished. According to the number of errors happened, their type we decide whether we
106         * want to save the last query in OverpassQueryList.
107         */
108        Consumer<Collection> errorReporter = (errors) -> {
109
110            boolean onlyNoDataError = errors.size() == 1 &&
111                    errors.contains("No data found in this area.");
112
113            if (errors.isEmpty() || onlyNoDataError) {
114                dialog.saveHistoricItemOnSuccess();
115            }
116        };
117
118        /*
119         * In order to support queries generated by the Overpass Turbo Query Wizard tool
120         * which do not require the area to be specified.
121         */
122        Bounds area = selectedArea.orElseGet(() -> new Bounds(0, 0, 0, 0));
123        DownloadOsmTask task = new DownloadOsmTask();
124        task.setZoomAfterDownload(dialog.isZoomToDownloadedDataRequired());
125        Future<?> future = task.download(
126                new OverpassDownloadReader(area, OverpassServerPreference.getOverpassServer(), dialog.getOverpassQuery()),
127                dialog.isNewLayerRequired(), area, null);
128        Main.worker.submit(new PostDownloadHandler(task, future, errorReporter));
129    }
130
131    private static final class DisableActionsFocusListener implements FocusListener {
132
133        private final ActionMap actionMap;
134
135        private DisableActionsFocusListener(ActionMap actionMap) {
136            this.actionMap = actionMap;
137        }
138
139        @Override
140        public void focusGained(FocusEvent e) {
141            enableActions(false);
142        }
143
144        @Override
145        public void focusLost(FocusEvent e) {
146            enableActions(true);
147        }
148
149        private void enableActions(boolean enabled) {
150            Object[] allKeys = actionMap.allKeys();
151            if (allKeys != null) {
152                for (Object key : allKeys) {
153                    Action action = actionMap.get(key);
154                    if (action != null) {
155                        action.setEnabled(enabled);
156                    }
157                }
158            }
159        }
160    }
161
162    private static final class OverpassDownloadDialog extends DownloadDialog {
163
164        private JosmTextArea overpassQuery;
165        private OverpassQueryList overpassQueryList;
166        private static OverpassDownloadDialog instance;
167        private static final BooleanProperty OVERPASS_QUERY_LIST_OPENED =
168                new BooleanProperty("download.overpass.query-list.opened", false);
169
170        private OverpassDownloadDialog(Component parent) {
171            super(parent, ht("/Action/OverpassDownload"));
172            cbDownloadOsmData.setEnabled(false);
173            cbDownloadOsmData.setSelected(false);
174            cbDownloadGpxData.setVisible(false);
175            cbDownloadNotes.setVisible(false);
176            cbStartup.setVisible(false);
177        }
178
179        public static OverpassDownloadDialog getInstance() {
180            if (instance == null) {
181                instance = new OverpassDownloadDialog(Main.parent);
182            }
183            return instance;
184        }
185
186        @Override
187        protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
188            // needed for the invisible checkboxes cbDownloadGpxData, cbDownloadNotes
189            pnl.add(new JLabel(), GBC.eol());
190
191            DisableActionsFocusListener disableActionsFocusListener =
192                    new DisableActionsFocusListener(slippyMapChooser.getNavigationComponentActionMap());
193
194            String tooltip = tr("Build an Overpass query using the Overpass Turbo Query Wizard tool");
195            Action queryWizardAction = new AbstractAction() {
196                @Override
197                public void actionPerformed(ActionEvent e) {
198                    QueryWizardDialog.getInstance().showDialog();
199                }
200            };
201
202            JButton openQueryWizard = new JButton("Query Wizard");
203            openQueryWizard.setToolTipText(tooltip);
204            openQueryWizard.addActionListener(queryWizardAction);
205
206            // CHECKSTYLE.OFF: LineLength
207            this.overpassQuery = new JosmTextArea(
208                    "/*\n" +
209                    tr("Place your Overpass query below or generate one using the Overpass Turbo Query Wizard")
210                    + "\n*/",
211                    8, 80);
212            // CHECKSTYLE.ON: LineLength
213            this.overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery));
214            this.overpassQuery.addFocusListener(disableActionsFocusListener);
215            this.overpassQuery.addFocusListener(new FocusListener() {
216                @Override
217                public void focusGained(FocusEvent e) {
218                    overpassQuery.selectAll();
219                }
220
221                @Override
222                public void focusLost(FocusEvent e) {
223
224                }
225            });
226
227            this.overpassQueryList = new OverpassQueryList(this, this.overpassQuery);
228            overpassQueryList.setToolTipText(tr("Show/hide Overpass snippet list"));
229            overpassQueryList.setVisible(OVERPASS_QUERY_LIST_OPENED.get());
230            overpassQueryList.setPreferredSize(new Dimension(350, 300));
231            JScrollPane scrollPane = new JScrollPane(overpassQuery);
232            BasicArrowButton arrowButton = new BasicArrowButton(overpassQueryList.isVisible()
233                ? BasicArrowButton.EAST
234                : BasicArrowButton.WEST);
235            arrowButton.addActionListener(e ->  {
236                if (overpassQueryList.isVisible()) {
237                    overpassQueryList.setVisible(false);
238                    arrowButton.setDirection(BasicArrowButton.WEST);
239                    OVERPASS_QUERY_LIST_OPENED.put(false);
240                } else {
241                    overpassQueryList.setVisible(true);
242                    arrowButton.setDirection(BasicArrowButton.EAST);
243                    OVERPASS_QUERY_LIST_OPENED.put(false);
244                }
245            });
246
247            JPanel innerPanel = new JPanel(new BorderLayout());
248            innerPanel.add(scrollPane, BorderLayout.CENTER);
249            innerPanel.add(arrowButton, BorderLayout.EAST);
250
251            JPanel pane = new JPanel(new BorderLayout());
252            pane.add(innerPanel, BorderLayout.CENTER);
253            pane.add(overpassQueryList, BorderLayout.EAST);
254
255            GBC gbc = GBC.eol().fill(GBC.HORIZONTAL); gbc.ipady = 200;
256            pnl.add(openQueryWizard, GBC.std().insets(5, 5, 5, 5));
257            pnl.add(pane, gbc);
258        }
259
260        String getOverpassQuery() {
261            return overpassQuery.getText();
262        }
263
264        void setOverpassQuery(String text) {
265            overpassQuery.setText(text);
266        }
267
268        /**
269         * Adds the current query to {@link OverpassQueryList}.
270         */
271        void saveHistoricItemOnSuccess() {
272            overpassQueryList.saveHistoricItem(overpassQuery.getText());
273        }
274
275        @Override
276        protected void updateSizeCheck() {
277            displaySizeCheckResult(false);
278        }
279
280        /**
281         * Triggers the download action to fire.
282         */
283        private void triggerDownload() {
284            super.btnDownload.doClick();
285        }
286    }
287
288    private static final class QueryWizardDialog extends ExtendedDialog {
289
290        private static QueryWizardDialog dialog;
291        private final HistoryComboBox queryWizard;
292        private final OverpassTurboQueryWizard overpassQueryBuilder;
293        private static final CollectionProperty OVERPASS_WIZARD_HISTORY =
294                new CollectionProperty("download.overpass.wizard", new ArrayList<String>());
295
296        // dialog buttons
297        private static final int BUILD_QUERY = 0;
298        private static final int BUILD_AN_EXECUTE_QUERY = 1;
299        private static final int CANCEL = 2;
300
301        /**
302         * Get an instance of {@link QueryWizardDialog}.
303         * @return The instance
304         */
305        public static QueryWizardDialog getInstance() {
306            if (dialog == null) {
307                dialog = new QueryWizardDialog();
308            }
309
310            return dialog;
311        }
312
313        private static final String DESCRIPTION_STYLE =
314                "<style type=\"text/css\">\n"
315                + "table { border-spacing: 0pt;}\n"
316                + "h3 {text-align: center; padding: 8px;}\n"
317                + "td {border: 1px solid #dddddd; text-align: left; padding: 8px;}\n"
318                + "#desc {width: 350px;}"
319                + "</style>\n";
320
321        private QueryWizardDialog() {
322            super(OverpassDownloadDialog.getInstance(), tr("Overpass Turbo Query Wizard"),
323                    tr("Build query"), tr("Build query and execute"), tr("Cancel"));
324
325            this.queryWizard = new HistoryComboBox();
326            this.overpassQueryBuilder = OverpassTurboQueryWizard.getInstance();
327
328            JPanel panel = new JPanel(new GridBagLayout());
329
330            JLabel searchLabel = new JLabel(tr("Search :"));
331            JTextComponent descPane = this.buildDescriptionSection();
332            JScrollPane scroll = GuiHelper.embedInVerticalScrollPane(descPane);
333            scroll.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
334
335            panel.add(searchLabel, GBC.std().insets(0, 0, 0, 20).anchor(GBC.SOUTHEAST));
336            panel.add(queryWizard, GBC.eol().insets(0, 0, 0, 15).fill(GBC.HORIZONTAL).anchor(GBC.SOUTH));
337            panel.add(scroll, GBC.eol().fill(GBC.BOTH).anchor(GBC.CENTER));
338
339            queryWizard.setPossibleItems(OVERPASS_WIZARD_HISTORY.get());
340
341            setCancelButton(CANCEL);
342            setDefaultButton(BUILD_AN_EXECUTE_QUERY + 1); // Build and execute button
343            setContent(panel, false);
344        }
345
346        @Override
347        public void buttonAction(int buttonIndex, ActionEvent evt) {
348            switch (buttonIndex) {
349                case BUILD_QUERY:
350                    if (this.buildQueryAction()) {
351                        this.saveHistory();
352                        super.buttonAction(BUILD_QUERY, evt);
353                    }
354                    break;
355                case BUILD_AN_EXECUTE_QUERY:
356                    if (this.buildQueryAction()) {
357                        this.saveHistory();
358                        super.buttonAction(BUILD_AN_EXECUTE_QUERY, evt);
359
360                        OverpassDownloadDialog.getInstance().triggerDownload();
361                    }
362                    break;
363                default:
364                    super.buttonAction(buttonIndex, evt);
365
366            }
367        }
368
369        /**
370         * Saves the latest, successfully parsed search term.
371         */
372        private void saveHistory() {
373            queryWizard.addCurrentItemToHistory();
374            OVERPASS_WIZARD_HISTORY.put(queryWizard.getHistory());
375        }
376
377        /**
378         * Tries to process a search term using {@link OverpassTurboQueryWizard}. If the term cannot
379         * be parsed, the the corresponding dialog is shown.
380         * @param searchTerm The search term to parse.
381         * @return {@link Optional#empty()} if an exception was thrown when parsing, meaning
382         * that the term cannot be processed, or non-empty {@link Optional} containing the result
383         * of parsing.
384         */
385        private Optional<String> tryParseSearchTerm(String searchTerm) {
386            try {
387                String query = this.overpassQueryBuilder.constructQuery(searchTerm);
388
389                return Optional.of(query);
390            } catch (UncheckedParseException ex) {
391                Main.error(ex);
392                JOptionPane.showMessageDialog(
393                        OverpassDownloadDialog.getInstance(),
394                        "<html>" +
395                         tr("The Overpass wizard could not parse the following query:") +
396                         Utils.joinAsHtmlUnorderedList(Collections.singleton(searchTerm)) +
397                         "</html>",
398                        tr("Parse error"),
399                        JOptionPane.ERROR_MESSAGE
400                );
401
402                return Optional.empty();
403            }
404        }
405
406        /**
407         * Builds an Overpass query out from {@link QueryWizardDialog#queryWizard} contents.
408         * @return {@code true} if the query successfully built, {@code false} otherwise.
409         */
410        private boolean buildQueryAction() {
411            final String wizardSearchTerm = this.queryWizard.getText();
412
413            Optional<String> q = this.tryParseSearchTerm(wizardSearchTerm);
414            if (q.isPresent()) {
415                String query = q.get();
416                OverpassDownloadDialog.getInstance().setOverpassQuery(query);
417
418                return true;
419            }
420
421            return false;
422        }
423
424        private JTextComponent buildDescriptionSection() {
425            JEditorPane descriptionSection = new JEditorPane("text/html", this.getDescriptionContent());
426            descriptionSection.setEditable(false);
427            descriptionSection.addHyperlinkListener(e -> {
428                if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
429                    OpenBrowser.displayUrl(e.getURL().toString());
430                }
431            });
432
433            return descriptionSection;
434        }
435
436        private String getDescriptionContent() {
437            return new StringBuilder("<html>")
438                    .append(DESCRIPTION_STYLE)
439                    .append("<body>")
440                    .append("<h3>")
441                    .append(tr("Query Wizard"))
442                    .append("</h3>")
443                    .append("<p>")
444                    .append(tr("Allows you to interact with <i>Overpass API</i> by writing declarative, human-readable terms."))
445                    .append(tr("The <i>Query Wizard</i> tool will transform those to a valid overpass query."))
446                    .append(tr("For more detailed description see "))
447                    .append(tr("<a href=\"{0}\">OSM Wiki</a>.", Main.getOSMWebsite() + "/wiki/Overpass_turbo/Wizard"))
448                    .append("</p>")
449                    .append("<h3>").append(tr("Hints")).append("</h3>")
450                    .append("<table>").append("<tr>").append("<td>")
451                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList("<i>type:node</i>", "<i>type:relation</i>", "<i>type:way</i>")))
452                    .append("</td>").append("<td>")
453                    .append("<span>").append(tr("Download objects of a certain type.")).append("</span>")
454                    .append("</td>").append("</tr>")
455                    .append("<tr>").append("<td>")
456                    .append(Utils.joinAsHtmlUnorderedList(
457                            Arrays.asList("<i>key=value in <u>location</u></i>",
458                                    "<i>key=value around <u>location</u></i>",
459                                    "<i>key=value in bbox</i>")))
460                    .append("</td>").append("<td>")
461                    .append(tr("Download object by specifying a specific location. For example,"))
462                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList(
463                            tr("{0} all objects having {1} as attribute are downloaded.", "<i>tourism=hotel in Berlin</i> -", "'tourism=hotel'"),
464                            tr("{0} all object with the corresponding key/value pair located around Berlin. Note, the default value for radius "+
465                                    "is set to 1000m, but it can be changed in the generated query.", "<i>tourism=hotel around Berlin</i> -"),
466                            tr("{0} all objects within the current selection that have {1} as attribute.", "<i>tourism=hotel in bbox</i> -",
467                                    "'tourism=hotel'"))))
468                    .append("<span>")
469                    .append(tr("Instead of <i>location</i> any valid place name can be used like address, city, etc."))
470                    .append("</span>")
471                    .append("</td>").append("</tr>")
472                    .append("<tr>").append("<td>")
473                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList("<i>key=value</i>", "<i>key=*</i>", "<i>key~regex</i>",
474                            "<i>key!=value</i>", "<i>key!~regex</i>", "<i>key=\"combined value\"</i>")))
475                    .append("</td>").append("<td>")
476                    .append(tr("<span>Download objects that have some concrete key/value pair, only the key with any contents for the value, " +
477                            "the value matching some regular expression. 'Not equal' operators are supported as well.</span>"))
478                    .append("</td>").append("</tr>")
479                    .append("<tr>").append("<td>")
480                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList(
481                            tr("<i>expression1 {0} expression2</i>", "or"),
482                            tr("<i>expression1 {0} expression2</i>", "and"))))
483                    .append("</td>").append("<td>")
484                    .append("<span>")
485                    .append(tr("Basic logical operators can be used to create more sophisticated queries. Instead of 'or' - '|', '||' " +
486                            "can be used, and instead of 'and' - '&', '&&'."))
487                    .append("</span>")
488                    .append("</td>").append("</tr>").append("</table>")
489                    .append("</body>")
490                    .append("</html>")
491                    .toString();
492        }
493    }
494}
Note: See TracBrowser for help on using the repository browser.