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