source: josm/trunk/src/org/openstreetmap/josm/gui/download/OverpassDownloadSource.java@ 15447

Last change on this file since 15447 was 15447, checked in by GerdP, 5 years ago

see #18122: Don't set download bounds for incomplete Overpass queries
apply 18122-v1.patch so that Overpass queries don't set download bounds unless they are detected to return a complete result. For now, this is only detected when the user query is empty or just a comment.
TODO: This detection should also detect user written queries which return complete data

File size: 15.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.download;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Dimension;
8import java.awt.GridBagLayout;
9import java.awt.event.ActionEvent;
10import java.awt.event.FocusAdapter;
11import java.awt.event.FocusEvent;
12import java.util.Collection;
13import java.util.Objects;
14import java.util.concurrent.Future;
15import java.util.function.Consumer;
16
17import javax.swing.AbstractAction;
18import javax.swing.BorderFactory;
19import javax.swing.Icon;
20import javax.swing.JButton;
21import javax.swing.JLabel;
22import javax.swing.JOptionPane;
23import javax.swing.JPanel;
24import javax.swing.JScrollPane;
25import javax.swing.event.ListSelectionEvent;
26import javax.swing.event.ListSelectionListener;
27import javax.swing.plaf.basic.BasicArrowButton;
28
29import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
30import org.openstreetmap.josm.actions.downloadtasks.DownloadParams;
31import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
32import org.openstreetmap.josm.data.Bounds;
33import org.openstreetmap.josm.data.preferences.AbstractProperty;
34import org.openstreetmap.josm.data.preferences.BooleanProperty;
35import org.openstreetmap.josm.data.preferences.IntegerProperty;
36import org.openstreetmap.josm.data.preferences.StringProperty;
37import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
38import org.openstreetmap.josm.gui.MainApplication;
39import org.openstreetmap.josm.gui.download.DownloadSourceSizingPolicy.AdjustableDownloadSizePolicy;
40import org.openstreetmap.josm.gui.download.overpass.OverpassWizardRegistration;
41import org.openstreetmap.josm.gui.download.overpass.OverpassWizardRegistration.OverpassQueryWizard;
42import org.openstreetmap.josm.gui.download.overpass.OverpassWizardRegistration.OverpassWizardCallbacks;
43import org.openstreetmap.josm.gui.util.GuiHelper;
44import org.openstreetmap.josm.gui.widgets.JosmTextArea;
45import org.openstreetmap.josm.io.OverpassDownloadReader;
46import org.openstreetmap.josm.tools.GBC;
47import org.openstreetmap.josm.tools.ImageProvider;
48
49/**
50 * Class defines the way data is fetched from Overpass API.
51 * @since 12652
52 */
53public class OverpassDownloadSource implements DownloadSource<OverpassDownloadSource.OverpassDownloadData> {
54 /** Overpass query to retrieve all nodes and related parent objects, */
55 public static final String FULL_DOWNLOAD_QUERY = "[out:xml]; \n"
56 + "(\n"
57 + " node({{bbox}});\n"
58 + "<;\n"
59 + ");\n"
60 + "(._;>;);"
61 + "out meta;";
62
63 @Override
64 public AbstractDownloadSourcePanel<OverpassDownloadData> createPanel(DownloadDialog dialog) {
65 return new OverpassDownloadSourcePanel(this);
66 }
67
68 @Override
69 public void doDownload(OverpassDownloadData data, DownloadSettings settings) {
70 /*
71 * In order to support queries generated by the Overpass Turbo Query Wizard tool
72 * which do not require the area to be specified.
73 */
74 Bounds area = settings.getDownloadBounds().orElse(new Bounds(0, 0, 0, 0));
75 DownloadOsmTask task = new DownloadOsmTask();
76 task.setZoomAfterDownload(settings.zoomToData());
77 Future<?> future = task.download(
78 new OverpassDownloadReader(area, OverpassDownloadReader.OVERPASS_SERVER.get(), data.getQuery()),
79 new DownloadParams().withNewLayer(settings.asNewLayer()), area, null);
80 MainApplication.worker.submit(new PostDownloadHandler(task, future, data.getErrorReporter()));
81 }
82
83 @Override
84 public String getLabel() {
85 return tr("Download from Overpass API");
86 }
87
88 @Override
89 public boolean onlyExpert() {
90 return true;
91 }
92
93 /**
94 * The GUI representation of the Overpass download source.
95 * @since 12652
96 */
97 public static class OverpassDownloadSourcePanel extends AbstractDownloadSourcePanel<OverpassDownloadData>
98 implements OverpassWizardCallbacks {
99
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}
Note: See TracBrowser for help on using the repository browser.