001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.download;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Dimension;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.FocusAdapter;
011import java.awt.event.FocusEvent;
012import java.util.Collection;
013import java.util.concurrent.Future;
014import java.util.function.Consumer;
015
016import javax.swing.AbstractAction;
017import javax.swing.Icon;
018import javax.swing.JButton;
019import javax.swing.JLabel;
020import javax.swing.JOptionPane;
021import javax.swing.JPanel;
022import javax.swing.JScrollPane;
023import javax.swing.event.ListSelectionEvent;
024import javax.swing.event.ListSelectionListener;
025import javax.swing.plaf.basic.BasicArrowButton;
026
027import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
028import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
029import org.openstreetmap.josm.data.Bounds;
030import org.openstreetmap.josm.data.preferences.AbstractProperty;
031import org.openstreetmap.josm.data.preferences.BooleanProperty;
032import org.openstreetmap.josm.data.preferences.IntegerProperty;
033import org.openstreetmap.josm.data.preferences.StringProperty;
034import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
035import org.openstreetmap.josm.gui.MainApplication;
036import org.openstreetmap.josm.gui.download.DownloadSourceSizingPolicy.AdjustableDownloadSizePolicy;
037import org.openstreetmap.josm.gui.util.GuiHelper;
038import org.openstreetmap.josm.gui.widgets.JosmTextArea;
039import org.openstreetmap.josm.io.OverpassDownloadReader;
040import org.openstreetmap.josm.tools.GBC;
041import org.openstreetmap.josm.tools.ImageProvider;
042
043/**
044 * Class defines the way data is fetched from Overpass API.
045 * @since 12652
046 */
047public class OverpassDownloadSource implements DownloadSource<OverpassDownloadSource.OverpassDownloadData> {
048
049    @Override
050    public AbstractDownloadSourcePanel<OverpassDownloadData> createPanel(DownloadDialog dialog) {
051        return new OverpassDownloadSourcePanel(this);
052    }
053
054    @Override
055    public void doDownload(OverpassDownloadData data, DownloadSettings settings) {
056        /*
057         * In order to support queries generated by the Overpass Turbo Query Wizard tool
058         * which do not require the area to be specified.
059         */
060        Bounds area = settings.getDownloadBounds().orElse(new Bounds(0, 0, 0, 0));
061        DownloadOsmTask task = new DownloadOsmTask();
062        task.setZoomAfterDownload(settings.zoomToData());
063        Future<?> future = task.download(
064                new OverpassDownloadReader(area, OverpassDownloadReader.OVERPASS_SERVER.get(), data.getQuery()),
065                settings.asNewLayer(), area, null);
066        MainApplication.worker.submit(new PostDownloadHandler(task, future, data.getErrorReporter()));
067    }
068
069    @Override
070    public String getLabel() {
071        return tr("Download from Overpass API");
072    }
073
074    @Override
075    public boolean onlyExpert() {
076        return true;
077    }
078
079    /**
080     * The GUI representation of the Overpass download source.
081     * @since 12652
082     */
083    public static class OverpassDownloadSourcePanel extends AbstractDownloadSourcePanel<OverpassDownloadData> {
084
085        private static final String SIMPLE_NAME = "overpassdownloadpanel";
086        private static final AbstractProperty<Integer> PANEL_SIZE_PROPERTY =
087                new IntegerProperty(TAB_SPLIT_NAMESPACE + SIMPLE_NAME, 150).cached();
088        private static final BooleanProperty OVERPASS_QUERY_LIST_OPENED =
089                new BooleanProperty("download.overpass.query-list.opened", false);
090        private static final String ACTION_IMG_SUBDIR = "dialogs";
091
092        private static final StringProperty DOWNLOAD_QUERY = new StringProperty("download.overpass.query",
093                "/*\n" + tr("Place your Overpass query below or generate one using the Overpass Turbo Query Wizard") + "\n*/");
094
095        private final JosmTextArea overpassQuery;
096        private final UserQueryList overpassQueryList;
097
098        /**
099         * Create a new {@link OverpassDownloadSourcePanel}
100         * @param ds The download source to create the panel for
101         */
102        public OverpassDownloadSourcePanel(OverpassDownloadSource ds) {
103            super(ds);
104            setLayout(new BorderLayout());
105
106            String tooltip = tr("Build an Overpass query using the Overpass Turbo Query Wizard tool");
107
108            JButton openQueryWizard = new JButton(tr("Query Wizard"));
109            openQueryWizard.setToolTipText(tooltip);
110            openQueryWizard.addActionListener(new AbstractAction() {
111                @Override
112                public void actionPerformed(ActionEvent e) {
113                    new OverpassQueryWizardDialog(OverpassDownloadSourcePanel.this).showDialog();
114                }
115            });
116
117            this.overpassQuery = new JosmTextArea(DOWNLOAD_QUERY.get(), 8, 80);
118            this.overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery));
119            this.overpassQuery.addFocusListener(new FocusAdapter() {
120                @Override
121                public void focusGained(FocusEvent e) {
122                    overpassQuery.selectAll();
123                }
124            });
125
126            this.overpassQueryList = new UserQueryList(this, this.overpassQuery, "download.overpass.queries");
127            this.overpassQueryList.setPreferredSize(new Dimension(350, 300));
128
129            EditSnippetAction edit = new EditSnippetAction();
130            RemoveSnippetAction remove = new RemoveSnippetAction();
131            this.overpassQueryList.addSelectionListener(edit);
132            this.overpassQueryList.addSelectionListener(remove);
133
134            JPanel listPanel = new JPanel(new GridBagLayout());
135            listPanel.add(new JLabel(tr("Your saved queries:")), GBC.eol().insets(2).anchor(GBC.CENTER));
136            listPanel.add(this.overpassQueryList, GBC.eol().fill(GBC.BOTH));
137            listPanel.add(new JButton(new AddSnippetAction()), GBC.std().fill(GBC.HORIZONTAL));
138            listPanel.add(new JButton(edit), GBC.std().fill(GBC.HORIZONTAL));
139            listPanel.add(new JButton(remove), GBC.std().fill(GBC.HORIZONTAL));
140            listPanel.setVisible(OVERPASS_QUERY_LIST_OPENED.get());
141
142            JScrollPane scrollPane = new JScrollPane(overpassQuery);
143            BasicArrowButton arrowButton = new BasicArrowButton(listPanel.isVisible()
144                    ? BasicArrowButton.EAST
145                    : BasicArrowButton.WEST);
146            arrowButton.setToolTipText(tr("Show/hide Overpass snippet list"));
147            arrowButton.addActionListener(e -> {
148                if (listPanel.isVisible()) {
149                    listPanel.setVisible(false);
150                    arrowButton.setDirection(BasicArrowButton.WEST);
151                    OVERPASS_QUERY_LIST_OPENED.put(Boolean.FALSE);
152                } else {
153                    listPanel.setVisible(true);
154                    arrowButton.setDirection(BasicArrowButton.EAST);
155                    OVERPASS_QUERY_LIST_OPENED.put(Boolean.TRUE);
156                }
157            });
158
159            JPanel innerPanel = new JPanel(new BorderLayout());
160            innerPanel.add(scrollPane, BorderLayout.CENTER);
161            innerPanel.add(arrowButton, BorderLayout.EAST);
162
163            JPanel leftPanel = new JPanel(new GridBagLayout());
164            leftPanel.add(new JLabel(tr("Overpass query:")), GBC.eol().insets(5, 1, 5, 1).anchor(GBC.NORTHWEST));
165            leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL));
166            leftPanel.add(openQueryWizard, GBC.eol().anchor(GBC.CENTER));
167            leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL));
168
169            add(leftPanel, BorderLayout.WEST);
170            add(innerPanel, BorderLayout.CENTER);
171            add(listPanel, BorderLayout.EAST);
172
173            setMinimumSize(new Dimension(450, 240));
174        }
175
176        @Override
177        public OverpassDownloadData getData() {
178            String query = overpassQuery.getText();
179            /*
180             * A callback that is passed to PostDownloadReporter that is called once the download task
181             * has finished. According to the number of errors happened, their type we decide whether we
182             * want to save the last query in OverpassQueryList.
183             */
184            Consumer<Collection<Object>> errorReporter = errors -> {
185
186                boolean onlyNoDataError = errors.size() == 1 &&
187                        errors.contains("No data found in this area.");
188
189                if (errors.isEmpty() || onlyNoDataError) {
190                    overpassQueryList.saveHistoricItem(query);
191                }
192            };
193
194            return new OverpassDownloadData(OverpassDownloadReader.fixQuery(query), errorReporter);
195        }
196
197        @Override
198        public void rememberSettings() {
199            DOWNLOAD_QUERY.put(overpassQuery.getText());
200        }
201
202        @Override
203        public void restoreSettings() {
204            overpassQuery.setText(DOWNLOAD_QUERY.get());
205        }
206
207        @Override
208        public boolean checkDownload(DownloadSettings settings) {
209            String query = getData().getQuery();
210
211            /*
212             * Absence of the selected area can be justified only if the overpass query
213             * is not restricted to bbox.
214             */
215            if (!settings.getDownloadBounds().isPresent() && query.contains("{{bbox}}")) {
216                JOptionPane.showMessageDialog(
217                        this.getParent(),
218                        tr("Please select a download area first."),
219                        tr("Error"),
220                        JOptionPane.ERROR_MESSAGE
221                );
222                return false;
223            }
224
225            /*
226             * Check for an empty query. User might want to download everything, if so validation is passed,
227             * otherwise return false.
228             */
229            if (query.matches("(/\\*(\\*[^/]|[^\\*/])*\\*/|\\s)*")) {
230                boolean doFix = ConditionalOptionPaneUtil.showConfirmationDialog(
231                        "download.overpass.fix.emptytoall",
232                        this,
233                        tr("You entered an empty query. Do you want to download all data in this area instead?"),
234                        tr("Download all data?"),
235                        JOptionPane.YES_NO_OPTION,
236                        JOptionPane.QUESTION_MESSAGE,
237                        JOptionPane.YES_OPTION);
238                if (doFix) {
239                    String repairedQuery = "[out:xml]; \n"
240                            + query + "\n"
241                            + "(\n"
242                            + "    node({{bbox}});\n"
243                            + "<;\n"
244                            + ");\n"
245                            + "(._;>;);"
246                            + "out meta;";
247                    this.overpassQuery.setText(repairedQuery);
248                } else {
249                    return false;
250                }
251            }
252
253            return true;
254        }
255
256        /**
257         * Sets query to the query text field.
258         * @param query The query to set.
259         */
260        public void setOverpassQuery(String query) {
261            this.overpassQuery.setText(query);
262        }
263
264        @Override
265        public Icon getIcon() {
266            return ImageProvider.get("download-overpass");
267        }
268
269        @Override
270        public String getSimpleName() {
271            return SIMPLE_NAME;
272        }
273
274        @Override
275        public DownloadSourceSizingPolicy getSizingPolicy() {
276            return new AdjustableDownloadSizePolicy(PANEL_SIZE_PROPERTY);
277        }
278
279        /**
280         * Action that delegates snippet creation to {@link UserQueryList#createNewItem()}.
281         */
282        private class AddSnippetAction extends AbstractAction {
283
284            /**
285             * Constructs a new {@code AddSnippetAction}.
286             */
287            AddSnippetAction() {
288                new ImageProvider(ACTION_IMG_SUBDIR, "add").getResource().attachImageIcon(this, true);
289                putValue(SHORT_DESCRIPTION, tr("Add new snippet"));
290            }
291
292            @Override
293            public void actionPerformed(ActionEvent e) {
294                overpassQueryList.createNewItem();
295            }
296        }
297
298        /**
299         * Action that delegates snippet removal to {@link UserQueryList#removeSelectedItem()}.
300         */
301        private class RemoveSnippetAction extends AbstractAction implements ListSelectionListener {
302
303            /**
304             * Constructs a new {@code RemoveSnippetAction}.
305             */
306            RemoveSnippetAction() {
307                new ImageProvider(ACTION_IMG_SUBDIR, "delete").getResource().attachImageIcon(this, true);
308                putValue(SHORT_DESCRIPTION, tr("Delete selected snippet"));
309                checkEnabled();
310            }
311
312            @Override
313            public void actionPerformed(ActionEvent e) {
314                overpassQueryList.removeSelectedItem();
315            }
316
317            /**
318             * Disables the action if no items are selected.
319             */
320            void checkEnabled() {
321                setEnabled(overpassQueryList.getSelectedItem().isPresent());
322            }
323
324            @Override
325            public void valueChanged(ListSelectionEvent e) {
326                checkEnabled();
327            }
328        }
329
330        /**
331         * Action that delegates snippet edit to {@link UserQueryList#editSelectedItem()}.
332         */
333        private class EditSnippetAction extends AbstractAction implements ListSelectionListener {
334
335            /**
336             * Constructs a new {@code EditSnippetAction}.
337             */
338            EditSnippetAction() {
339                super();
340                new ImageProvider(ACTION_IMG_SUBDIR, "edit").getResource().attachImageIcon(this, true);
341                putValue(SHORT_DESCRIPTION, tr("Edit selected snippet"));
342                checkEnabled();
343            }
344
345            @Override
346            public void actionPerformed(ActionEvent e) {
347                overpassQueryList.editSelectedItem();
348            }
349
350            /**
351             * Disables the action if no items are selected.
352             */
353            void checkEnabled() {
354                setEnabled(overpassQueryList.getSelectedItem().isPresent());
355            }
356
357            @Override
358            public void valueChanged(ListSelectionEvent e) {
359                checkEnabled();
360            }
361        }
362    }
363
364    /**
365     * Encapsulates data that is required to preform download from Overpass API.
366     */
367    static class OverpassDownloadData {
368        private final String query;
369        private final Consumer<Collection<Object>> errorReporter;
370
371        OverpassDownloadData(String query, Consumer<Collection<Object>> errorReporter) {
372            this.query = query;
373            this.errorReporter = errorReporter;
374        }
375
376        String getQuery() {
377            return this.query;
378        }
379
380        Consumer<Collection<Object>> getErrorReporter() {
381            return this.errorReporter;
382        }
383    }
384}