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}