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