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