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.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    /** Overpass query to retrieve all nodes and related parent objects, */
054    public static final String FULL_DOWNLOAD_QUERY = "[out:xml]; \n"
055            + "(\n"
056            + "    node({{bbox}});\n"
057            + "<;\n"
058            + ");\n"
059            + "(._;>;);"
060            + "out meta;";
061
062    @Override
063    public AbstractDownloadSourcePanel<OverpassDownloadData> createPanel(DownloadDialog dialog) {
064        return new OverpassDownloadSourcePanel(this);
065    }
066
067    @Override
068    public void doDownload(OverpassDownloadData data, DownloadSettings settings) {
069        /*
070         * In order to support queries generated by the Overpass Turbo Query Wizard tool
071         * which do not require the area to be specified.
072         */
073        Bounds area = settings.getDownloadBounds().orElse(new Bounds(0, 0, 0, 0));
074        DownloadOsmTask task = new DownloadOsmTask();
075        task.setZoomAfterDownload(settings.zoomToData());
076        Future<?> future = task.download(
077                new OverpassDownloadReader(area, OverpassDownloadReader.OVERPASS_SERVER.get(), data.getQuery()),
078                new DownloadParams().withNewLayer(settings.asNewLayer()), area, null);
079        MainApplication.worker.submit(new PostDownloadHandler(task, future, data.getErrorReporter()));
080    }
081
082    @Override
083    public String getLabel() {
084        return tr("Download from Overpass API");
085    }
086
087    @Override
088    public boolean onlyExpert() {
089        return true;
090    }
091
092    /**
093     * The GUI representation of the Overpass download source.
094     * @since 12652
095     */
096    public static class OverpassDownloadSourcePanel extends AbstractDownloadSourcePanel<OverpassDownloadData>
097            implements OverpassWizardCallbacks {
098
099        private static final String SIMPLE_NAME = "overpassdownloadpanel";
100        private static final AbstractProperty<Integer> PANEL_SIZE_PROPERTY =
101                new IntegerProperty(TAB_SPLIT_NAMESPACE + SIMPLE_NAME, 150).cached();
102        private static final BooleanProperty OVERPASS_QUERY_LIST_OPENED =
103                new BooleanProperty("download.overpass.query-list.opened", false);
104        private static final String ACTION_IMG_SUBDIR = "dialogs";
105
106        private static final StringProperty DOWNLOAD_QUERY = new StringProperty("download.overpass.query",
107                "/*\n" + tr("Place your Overpass query below or generate one using the query wizard") + "\n*/");
108
109        private final JosmTextArea overpassQuery;
110        private final UserQueryList overpassQueryList;
111
112        /**
113         * Create a new {@link OverpassDownloadSourcePanel}
114         * @param ds The download source to create the panel for
115         */
116        public OverpassDownloadSourcePanel(OverpassDownloadSource ds) {
117            super(ds);
118            setLayout(new BorderLayout());
119
120            this.overpassQuery = new JosmTextArea(DOWNLOAD_QUERY.get(), 8, 80);
121            this.overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery));
122            this.overpassQuery.addFocusListener(new FocusAdapter() {
123                @Override
124                public void focusGained(FocusEvent e) {
125                    overpassQuery.selectAll();
126                }
127            });
128
129            this.overpassQueryList = new UserQueryList(this, this.overpassQuery, "download.overpass.queries");
130            this.overpassQueryList.setPreferredSize(new Dimension(350, 300));
131
132            EditSnippetAction edit = new EditSnippetAction();
133            RemoveSnippetAction remove = new RemoveSnippetAction();
134            this.overpassQueryList.addSelectionListener(edit);
135            this.overpassQueryList.addSelectionListener(remove);
136
137            JPanel listPanel = new JPanel(new GridBagLayout());
138            listPanel.add(new JLabel(tr("Your saved queries:")), GBC.eol().insets(2).anchor(GBC.CENTER));
139            listPanel.add(this.overpassQueryList, GBC.eol().fill(GBC.BOTH));
140            listPanel.add(new JButton(new AddSnippetAction()), GBC.std().fill(GBC.HORIZONTAL));
141            listPanel.add(new JButton(edit), GBC.std().fill(GBC.HORIZONTAL));
142            listPanel.add(new JButton(remove), GBC.std().fill(GBC.HORIZONTAL));
143            listPanel.setVisible(OVERPASS_QUERY_LIST_OPENED.get());
144
145            JScrollPane scrollPane = new JScrollPane(overpassQuery);
146            BasicArrowButton arrowButton = new BasicArrowButton(listPanel.isVisible()
147                    ? BasicArrowButton.EAST
148                    : BasicArrowButton.WEST);
149            arrowButton.setToolTipText(tr("Show/hide Overpass snippet list"));
150            arrowButton.addActionListener(e -> {
151                if (listPanel.isVisible()) {
152                    listPanel.setVisible(false);
153                    arrowButton.setDirection(BasicArrowButton.WEST);
154                    OVERPASS_QUERY_LIST_OPENED.put(Boolean.FALSE);
155                } else {
156                    listPanel.setVisible(true);
157                    arrowButton.setDirection(BasicArrowButton.EAST);
158                    OVERPASS_QUERY_LIST_OPENED.put(Boolean.TRUE);
159                }
160            });
161
162            JPanel innerPanel = new JPanel(new BorderLayout());
163            innerPanel.add(scrollPane, BorderLayout.CENTER);
164            innerPanel.add(arrowButton, BorderLayout.EAST);
165
166            JPanel leftPanel = new JPanel(new GridBagLayout());
167            leftPanel.add(new JLabel(tr("Overpass query:")), GBC.eol().insets(5, 1, 5, 1).anchor(GBC.NORTHWEST));
168            leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL));
169            OverpassWizardRegistration.getWizards(this)
170                .stream()
171                .map(JButton::new)
172                .forEach(button -> leftPanel.add(button, GBC.eol().anchor(GBC.CENTER)));
173            leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL));
174            leftPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
175
176            add(leftPanel, BorderLayout.WEST);
177            add(innerPanel, BorderLayout.CENTER);
178            add(listPanel, BorderLayout.EAST);
179
180            setMinimumSize(new Dimension(450, 240));
181        }
182
183        @Override
184        public OverpassDownloadData getData() {
185            String query = overpassQuery.getText();
186            /*
187             * A callback that is passed to PostDownloadReporter that is called once the download task
188             * has finished. According to the number of errors happened, their type we decide whether we
189             * want to save the last query in OverpassQueryList.
190             */
191            Consumer<Collection<Object>> errorReporter = errors -> {
192
193                boolean onlyNoDataError = errors.size() == 1 &&
194                        errors.contains("No data found in this area.");
195
196                if (errors.isEmpty() || onlyNoDataError) {
197                    overpassQueryList.saveHistoricItem(query);
198                }
199            };
200
201            return new OverpassDownloadData(OverpassDownloadReader.fixQuery(query), errorReporter);
202        }
203
204        @Override
205        public void rememberSettings() {
206            DOWNLOAD_QUERY.put(overpassQuery.getText());
207        }
208
209        @Override
210        public void restoreSettings() {
211            overpassQuery.setText(DOWNLOAD_QUERY.get());
212        }
213
214        @Override
215        public boolean checkDownload(DownloadSettings settings) {
216            String query = getData().getQuery();
217
218            /*
219             * Absence of the selected area can be justified only if the overpass query
220             * is not restricted to bbox.
221             */
222            if (!settings.getDownloadBounds().isPresent() && query.contains("{{bbox}}")) {
223                JOptionPane.showMessageDialog(
224                        this.getParent(),
225                        tr("Please select a download area first."),
226                        tr("Error"),
227                        JOptionPane.ERROR_MESSAGE
228                );
229                return false;
230            }
231
232            /*
233             * Check for an empty query. User might want to download everything, if so validation is passed,
234             * otherwise return false.
235             */
236            if (query.matches("(/\\*(\\*[^/]|[^\\*/])*\\*/|\\s)*")) {
237                boolean doFix = ConditionalOptionPaneUtil.showConfirmationDialog(
238                        "download.overpass.fix.emptytoall",
239                        this,
240                        tr("You entered an empty query. Do you want to download all data in this area instead?"),
241                        tr("Download all data?"),
242                        JOptionPane.YES_NO_OPTION,
243                        JOptionPane.QUESTION_MESSAGE,
244                        JOptionPane.YES_OPTION);
245                if (doFix) {
246                    this.overpassQuery.setText(FULL_DOWNLOAD_QUERY);
247                } else {
248                    return false;
249                }
250            }
251
252            return true;
253        }
254
255        /**
256         * Sets query to the query text field.
257         * @param query The query to set.
258         */
259        public void setOverpassQuery(String query) {
260            Objects.requireNonNull(query, "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, () -> 50);
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        @Override
364        public void submitWizardResult(String resultingQuery) {
365            setOverpassQuery(resultingQuery);
366        }
367    }
368
369    /**
370     * Encapsulates data that is required to preform download from Overpass API.
371     */
372    static class OverpassDownloadData {
373        private final String query;
374        private final Consumer<Collection<Object>> errorReporter;
375
376        OverpassDownloadData(String query, Consumer<Collection<Object>> errorReporter) {
377            this.query = query;
378            this.errorReporter = errorReporter;
379        }
380
381        String getQuery() {
382            return this.query;
383        }
384
385        Consumer<Collection<Object>> getErrorReporter() {
386            return this.errorReporter;
387        }
388    }
389
390}