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.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 /** Overpass query to retrieve all nodes and related parent objects, */ 054 public static final String FULL_DOWNLOAD_QUERY = "[out:xml]; \n" 055 + "(\n" 056 + " node({{bbox}});\n" 057 + "<;\n" 058 + ");\n" 059 + "(._;>;);" 060 + "out meta;"; 061 062 @Override 063 public AbstractDownloadSourcePanel<OverpassDownloadData> createPanel(DownloadDialog dialog) { 064 return new OverpassDownloadSourcePanel(this); 065 } 066 067 @Override 068 public void doDownload(OverpassDownloadData data, DownloadSettings settings) { 069 /* 070 * In order to support queries generated by the Overpass Turbo Query Wizard tool 071 * which do not require the area to be specified. 072 */ 073 Bounds area = settings.getDownloadBounds().orElse(new Bounds(0, 0, 0, 0)); 074 DownloadOsmTask task = new DownloadOsmTask(); 075 task.setZoomAfterDownload(settings.zoomToData()); 076 Future<?> future = task.download( 077 new OverpassDownloadReader(area, OverpassDownloadReader.OVERPASS_SERVER.get(), data.getQuery()), 078 new DownloadParams().withNewLayer(settings.asNewLayer()), area, null); 079 MainApplication.worker.submit(new PostDownloadHandler(task, future, data.getErrorReporter())); 080 } 081 082 @Override 083 public String getLabel() { 084 return tr("Download from Overpass API"); 085 } 086 087 @Override 088 public boolean onlyExpert() { 089 return true; 090 } 091 092 /** 093 * The GUI representation of the Overpass download source. 094 * @since 12652 095 */ 096 public static class OverpassDownloadSourcePanel extends AbstractDownloadSourcePanel<OverpassDownloadData> 097 implements OverpassWizardCallbacks { 098 099 private static final String SIMPLE_NAME = "overpassdownloadpanel"; 100 private static final AbstractProperty<Integer> PANEL_SIZE_PROPERTY = 101 new IntegerProperty(TAB_SPLIT_NAMESPACE + SIMPLE_NAME, 150).cached(); 102 private static final BooleanProperty OVERPASS_QUERY_LIST_OPENED = 103 new BooleanProperty("download.overpass.query-list.opened", false); 104 private static final String ACTION_IMG_SUBDIR = "dialogs"; 105 106 private static final StringProperty DOWNLOAD_QUERY = new StringProperty("download.overpass.query", 107 "/*\n" + tr("Place your Overpass query below or generate one using the query wizard") + "\n*/"); 108 109 private final JosmTextArea overpassQuery; 110 private final UserQueryList overpassQueryList; 111 112 /** 113 * Create a new {@link OverpassDownloadSourcePanel} 114 * @param ds The download source to create the panel for 115 */ 116 public OverpassDownloadSourcePanel(OverpassDownloadSource ds) { 117 super(ds); 118 setLayout(new BorderLayout()); 119 120 this.overpassQuery = new JosmTextArea(DOWNLOAD_QUERY.get(), 8, 80); 121 this.overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery)); 122 this.overpassQuery.addFocusListener(new FocusAdapter() { 123 @Override 124 public void focusGained(FocusEvent e) { 125 overpassQuery.selectAll(); 126 } 127 }); 128 129 this.overpassQueryList = new UserQueryList(this, this.overpassQuery, "download.overpass.queries"); 130 this.overpassQueryList.setPreferredSize(new Dimension(350, 300)); 131 132 EditSnippetAction edit = new EditSnippetAction(); 133 RemoveSnippetAction remove = new RemoveSnippetAction(); 134 this.overpassQueryList.addSelectionListener(edit); 135 this.overpassQueryList.addSelectionListener(remove); 136 137 JPanel listPanel = new JPanel(new GridBagLayout()); 138 listPanel.add(new JLabel(tr("Your saved queries:")), GBC.eol().insets(2).anchor(GBC.CENTER)); 139 listPanel.add(this.overpassQueryList, GBC.eol().fill(GBC.BOTH)); 140 listPanel.add(new JButton(new AddSnippetAction()), GBC.std().fill(GBC.HORIZONTAL)); 141 listPanel.add(new JButton(edit), GBC.std().fill(GBC.HORIZONTAL)); 142 listPanel.add(new JButton(remove), GBC.std().fill(GBC.HORIZONTAL)); 143 listPanel.setVisible(OVERPASS_QUERY_LIST_OPENED.get()); 144 145 JScrollPane scrollPane = new JScrollPane(overpassQuery); 146 BasicArrowButton arrowButton = new BasicArrowButton(listPanel.isVisible() 147 ? BasicArrowButton.EAST 148 : BasicArrowButton.WEST); 149 arrowButton.setToolTipText(tr("Show/hide Overpass snippet list")); 150 arrowButton.addActionListener(e -> { 151 if (listPanel.isVisible()) { 152 listPanel.setVisible(false); 153 arrowButton.setDirection(BasicArrowButton.WEST); 154 OVERPASS_QUERY_LIST_OPENED.put(Boolean.FALSE); 155 } else { 156 listPanel.setVisible(true); 157 arrowButton.setDirection(BasicArrowButton.EAST); 158 OVERPASS_QUERY_LIST_OPENED.put(Boolean.TRUE); 159 } 160 }); 161 162 JPanel innerPanel = new JPanel(new BorderLayout()); 163 innerPanel.add(scrollPane, BorderLayout.CENTER); 164 innerPanel.add(arrowButton, BorderLayout.EAST); 165 166 JPanel leftPanel = new JPanel(new GridBagLayout()); 167 leftPanel.add(new JLabel(tr("Overpass query:")), GBC.eol().insets(5, 1, 5, 1).anchor(GBC.NORTHWEST)); 168 leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL)); 169 OverpassWizardRegistration.getWizards(this) 170 .stream() 171 .map(JButton::new) 172 .forEach(button -> leftPanel.add(button, GBC.eol().anchor(GBC.CENTER))); 173 leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL)); 174 leftPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 175 176 add(leftPanel, BorderLayout.WEST); 177 add(innerPanel, BorderLayout.CENTER); 178 add(listPanel, BorderLayout.EAST); 179 180 setMinimumSize(new Dimension(450, 240)); 181 } 182 183 @Override 184 public OverpassDownloadData getData() { 185 String query = overpassQuery.getText(); 186 /* 187 * A callback that is passed to PostDownloadReporter that is called once the download task 188 * has finished. According to the number of errors happened, their type we decide whether we 189 * want to save the last query in OverpassQueryList. 190 */ 191 Consumer<Collection<Object>> errorReporter = errors -> { 192 193 boolean onlyNoDataError = errors.size() == 1 && 194 errors.contains("No data found in this area."); 195 196 if (errors.isEmpty() || onlyNoDataError) { 197 overpassQueryList.saveHistoricItem(query); 198 } 199 }; 200 201 return new OverpassDownloadData(OverpassDownloadReader.fixQuery(query), errorReporter); 202 } 203 204 @Override 205 public void rememberSettings() { 206 DOWNLOAD_QUERY.put(overpassQuery.getText()); 207 } 208 209 @Override 210 public void restoreSettings() { 211 overpassQuery.setText(DOWNLOAD_QUERY.get()); 212 } 213 214 @Override 215 public boolean checkDownload(DownloadSettings settings) { 216 String query = getData().getQuery(); 217 218 /* 219 * Absence of the selected area can be justified only if the overpass query 220 * is not restricted to bbox. 221 */ 222 if (!settings.getDownloadBounds().isPresent() && query.contains("{{bbox}}")) { 223 JOptionPane.showMessageDialog( 224 this.getParent(), 225 tr("Please select a download area first."), 226 tr("Error"), 227 JOptionPane.ERROR_MESSAGE 228 ); 229 return false; 230 } 231 232 /* 233 * Check for an empty query. User might want to download everything, if so validation is passed, 234 * otherwise return false. 235 */ 236 if (query.matches("(/\\*(\\*[^/]|[^\\*/])*\\*/|\\s)*")) { 237 boolean doFix = ConditionalOptionPaneUtil.showConfirmationDialog( 238 "download.overpass.fix.emptytoall", 239 this, 240 tr("You entered an empty query. Do you want to download all data in this area instead?"), 241 tr("Download all data?"), 242 JOptionPane.YES_NO_OPTION, 243 JOptionPane.QUESTION_MESSAGE, 244 JOptionPane.YES_OPTION); 245 if (doFix) { 246 this.overpassQuery.setText(FULL_DOWNLOAD_QUERY); 247 } else { 248 return false; 249 } 250 } 251 252 return true; 253 } 254 255 /** 256 * Sets query to the query text field. 257 * @param query The query to set. 258 */ 259 public void setOverpassQuery(String query) { 260 Objects.requireNonNull(query, "query"); 261 this.overpassQuery.setText(query); 262 } 263 264 @Override 265 public Icon getIcon() { 266 return ImageProvider.get("download-overpass"); 267 } 268 269 @Override 270 public String getSimpleName() { 271 return SIMPLE_NAME; 272 } 273 274 @Override 275 public DownloadSourceSizingPolicy getSizingPolicy() { 276 return new AdjustableDownloadSizePolicy(PANEL_SIZE_PROPERTY, () -> 50); 277 } 278 279 /** 280 * Action that delegates snippet creation to {@link UserQueryList#createNewItem()}. 281 */ 282 private class AddSnippetAction extends AbstractAction { 283 284 /** 285 * Constructs a new {@code AddSnippetAction}. 286 */ 287 AddSnippetAction() { 288 new ImageProvider(ACTION_IMG_SUBDIR, "add").getResource().attachImageIcon(this, true); 289 putValue(SHORT_DESCRIPTION, tr("Add new snippet")); 290 } 291 292 @Override 293 public void actionPerformed(ActionEvent e) { 294 overpassQueryList.createNewItem(); 295 } 296 } 297 298 /** 299 * Action that delegates snippet removal to {@link UserQueryList#removeSelectedItem()}. 300 */ 301 private class RemoveSnippetAction extends AbstractAction implements ListSelectionListener { 302 303 /** 304 * Constructs a new {@code RemoveSnippetAction}. 305 */ 306 RemoveSnippetAction() { 307 new ImageProvider(ACTION_IMG_SUBDIR, "delete").getResource().attachImageIcon(this, true); 308 putValue(SHORT_DESCRIPTION, tr("Delete selected snippet")); 309 checkEnabled(); 310 } 311 312 @Override 313 public void actionPerformed(ActionEvent e) { 314 overpassQueryList.removeSelectedItem(); 315 } 316 317 /** 318 * Disables the action if no items are selected. 319 */ 320 void checkEnabled() { 321 setEnabled(overpassQueryList.getSelectedItem().isPresent()); 322 } 323 324 @Override 325 public void valueChanged(ListSelectionEvent e) { 326 checkEnabled(); 327 } 328 } 329 330 /** 331 * Action that delegates snippet edit to {@link UserQueryList#editSelectedItem()}. 332 */ 333 private class EditSnippetAction extends AbstractAction implements ListSelectionListener { 334 335 /** 336 * Constructs a new {@code EditSnippetAction}. 337 */ 338 EditSnippetAction() { 339 super(); 340 new ImageProvider(ACTION_IMG_SUBDIR, "edit").getResource().attachImageIcon(this, true); 341 putValue(SHORT_DESCRIPTION, tr("Edit selected snippet")); 342 checkEnabled(); 343 } 344 345 @Override 346 public void actionPerformed(ActionEvent e) { 347 overpassQueryList.editSelectedItem(); 348 } 349 350 /** 351 * Disables the action if no items are selected. 352 */ 353 void checkEnabled() { 354 setEnabled(overpassQueryList.getSelectedItem().isPresent()); 355 } 356 357 @Override 358 public void valueChanged(ListSelectionEvent e) { 359 checkEnabled(); 360 } 361 } 362 363 @Override 364 public void submitWizardResult(String resultingQuery) { 365 setOverpassQuery(resultingQuery); 366 } 367 } 368 369 /** 370 * Encapsulates data that is required to preform download from Overpass API. 371 */ 372 static class OverpassDownloadData { 373 private final String query; 374 private final Consumer<Collection<Object>> errorReporter; 375 376 OverpassDownloadData(String query, Consumer<Collection<Object>> errorReporter) { 377 this.query = query; 378 this.errorReporter = errorReporter; 379 } 380 381 String getQuery() { 382 return this.query; 383 } 384 385 Consumer<Collection<Object>> getErrorReporter() { 386 return this.errorReporter; 387 } 388 } 389 390}