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}