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