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.concurrent.Future; 014import java.util.function.Consumer; 015 016import javax.swing.AbstractAction; 017import javax.swing.Icon; 018import javax.swing.JButton; 019import javax.swing.JLabel; 020import javax.swing.JOptionPane; 021import javax.swing.JPanel; 022import javax.swing.JScrollPane; 023import javax.swing.event.ListSelectionEvent; 024import javax.swing.event.ListSelectionListener; 025import javax.swing.plaf.basic.BasicArrowButton; 026 027import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 028import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 029import org.openstreetmap.josm.data.Bounds; 030import org.openstreetmap.josm.data.preferences.AbstractProperty; 031import org.openstreetmap.josm.data.preferences.BooleanProperty; 032import org.openstreetmap.josm.data.preferences.IntegerProperty; 033import org.openstreetmap.josm.data.preferences.StringProperty; 034import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 035import org.openstreetmap.josm.gui.MainApplication; 036import org.openstreetmap.josm.gui.download.DownloadSourceSizingPolicy.AdjustableDownloadSizePolicy; 037import org.openstreetmap.josm.gui.util.GuiHelper; 038import org.openstreetmap.josm.gui.widgets.JosmTextArea; 039import org.openstreetmap.josm.io.OverpassDownloadReader; 040import org.openstreetmap.josm.tools.GBC; 041import org.openstreetmap.josm.tools.ImageProvider; 042 043/** 044 * Class defines the way data is fetched from Overpass API. 045 * @since 12652 046 */ 047public class OverpassDownloadSource implements DownloadSource<OverpassDownloadSource.OverpassDownloadData> { 048 049 @Override 050 public AbstractDownloadSourcePanel<OverpassDownloadData> createPanel(DownloadDialog dialog) { 051 return new OverpassDownloadSourcePanel(this); 052 } 053 054 @Override 055 public void doDownload(OverpassDownloadData data, DownloadSettings settings) { 056 /* 057 * In order to support queries generated by the Overpass Turbo Query Wizard tool 058 * which do not require the area to be specified. 059 */ 060 Bounds area = settings.getDownloadBounds().orElse(new Bounds(0, 0, 0, 0)); 061 DownloadOsmTask task = new DownloadOsmTask(); 062 task.setZoomAfterDownload(settings.zoomToData()); 063 Future<?> future = task.download( 064 new OverpassDownloadReader(area, OverpassDownloadReader.OVERPASS_SERVER.get(), data.getQuery()), 065 settings.asNewLayer(), area, null); 066 MainApplication.worker.submit(new PostDownloadHandler(task, future, data.getErrorReporter())); 067 } 068 069 @Override 070 public String getLabel() { 071 return tr("Download from Overpass API"); 072 } 073 074 @Override 075 public boolean onlyExpert() { 076 return true; 077 } 078 079 /** 080 * The GUI representation of the Overpass download source. 081 * @since 12652 082 */ 083 public static class OverpassDownloadSourcePanel extends AbstractDownloadSourcePanel<OverpassDownloadData> { 084 085 private static final String SIMPLE_NAME = "overpassdownloadpanel"; 086 private static final AbstractProperty<Integer> PANEL_SIZE_PROPERTY = 087 new IntegerProperty(TAB_SPLIT_NAMESPACE + SIMPLE_NAME, 150).cached(); 088 private static final BooleanProperty OVERPASS_QUERY_LIST_OPENED = 089 new BooleanProperty("download.overpass.query-list.opened", false); 090 private static final String ACTION_IMG_SUBDIR = "dialogs"; 091 092 private static final StringProperty DOWNLOAD_QUERY = new StringProperty("download.overpass.query", 093 "/*\n" + tr("Place your Overpass query below or generate one using the Overpass Turbo Query Wizard") + "\n*/"); 094 095 private final JosmTextArea overpassQuery; 096 private final UserQueryList overpassQueryList; 097 098 /** 099 * Create a new {@link OverpassDownloadSourcePanel} 100 * @param ds The download source to create the panel for 101 */ 102 public OverpassDownloadSourcePanel(OverpassDownloadSource ds) { 103 super(ds); 104 setLayout(new BorderLayout()); 105 106 String tooltip = tr("Build an Overpass query using the Overpass Turbo Query Wizard tool"); 107 108 JButton openQueryWizard = new JButton(tr("Query Wizard")); 109 openQueryWizard.setToolTipText(tooltip); 110 openQueryWizard.addActionListener(new AbstractAction() { 111 @Override 112 public void actionPerformed(ActionEvent e) { 113 new OverpassQueryWizardDialog(OverpassDownloadSourcePanel.this).showDialog(); 114 } 115 }); 116 117 this.overpassQuery = new JosmTextArea(DOWNLOAD_QUERY.get(), 8, 80); 118 this.overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery)); 119 this.overpassQuery.addFocusListener(new FocusAdapter() { 120 @Override 121 public void focusGained(FocusEvent e) { 122 overpassQuery.selectAll(); 123 } 124 }); 125 126 this.overpassQueryList = new UserQueryList(this, this.overpassQuery, "download.overpass.queries"); 127 this.overpassQueryList.setPreferredSize(new Dimension(350, 300)); 128 129 EditSnippetAction edit = new EditSnippetAction(); 130 RemoveSnippetAction remove = new RemoveSnippetAction(); 131 this.overpassQueryList.addSelectionListener(edit); 132 this.overpassQueryList.addSelectionListener(remove); 133 134 JPanel listPanel = new JPanel(new GridBagLayout()); 135 listPanel.add(new JLabel(tr("Your saved queries:")), GBC.eol().insets(2).anchor(GBC.CENTER)); 136 listPanel.add(this.overpassQueryList, GBC.eol().fill(GBC.BOTH)); 137 listPanel.add(new JButton(new AddSnippetAction()), GBC.std().fill(GBC.HORIZONTAL)); 138 listPanel.add(new JButton(edit), GBC.std().fill(GBC.HORIZONTAL)); 139 listPanel.add(new JButton(remove), GBC.std().fill(GBC.HORIZONTAL)); 140 listPanel.setVisible(OVERPASS_QUERY_LIST_OPENED.get()); 141 142 JScrollPane scrollPane = new JScrollPane(overpassQuery); 143 BasicArrowButton arrowButton = new BasicArrowButton(listPanel.isVisible() 144 ? BasicArrowButton.EAST 145 : BasicArrowButton.WEST); 146 arrowButton.setToolTipText(tr("Show/hide Overpass snippet list")); 147 arrowButton.addActionListener(e -> { 148 if (listPanel.isVisible()) { 149 listPanel.setVisible(false); 150 arrowButton.setDirection(BasicArrowButton.WEST); 151 OVERPASS_QUERY_LIST_OPENED.put(Boolean.FALSE); 152 } else { 153 listPanel.setVisible(true); 154 arrowButton.setDirection(BasicArrowButton.EAST); 155 OVERPASS_QUERY_LIST_OPENED.put(Boolean.TRUE); 156 } 157 }); 158 159 JPanel innerPanel = new JPanel(new BorderLayout()); 160 innerPanel.add(scrollPane, BorderLayout.CENTER); 161 innerPanel.add(arrowButton, BorderLayout.EAST); 162 163 JPanel leftPanel = new JPanel(new GridBagLayout()); 164 leftPanel.add(new JLabel(tr("Overpass query:")), GBC.eol().insets(5, 1, 5, 1).anchor(GBC.NORTHWEST)); 165 leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL)); 166 leftPanel.add(openQueryWizard, GBC.eol().anchor(GBC.CENTER)); 167 leftPanel.add(new JLabel(), GBC.eol().fill(GBC.VERTICAL)); 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 @Override 177 public OverpassDownloadData getData() { 178 String query = overpassQuery.getText(); 179 /* 180 * A callback that is passed to PostDownloadReporter that is called once the download task 181 * has finished. According to the number of errors happened, their type we decide whether we 182 * want to save the last query in OverpassQueryList. 183 */ 184 Consumer<Collection<Object>> errorReporter = errors -> { 185 186 boolean onlyNoDataError = errors.size() == 1 && 187 errors.contains("No data found in this area."); 188 189 if (errors.isEmpty() || onlyNoDataError) { 190 overpassQueryList.saveHistoricItem(query); 191 } 192 }; 193 194 return new OverpassDownloadData(OverpassDownloadReader.fixQuery(query), errorReporter); 195 } 196 197 @Override 198 public void rememberSettings() { 199 DOWNLOAD_QUERY.put(overpassQuery.getText()); 200 } 201 202 @Override 203 public void restoreSettings() { 204 overpassQuery.setText(DOWNLOAD_QUERY.get()); 205 } 206 207 @Override 208 public boolean checkDownload(DownloadSettings settings) { 209 String query = getData().getQuery(); 210 211 /* 212 * Absence of the selected area can be justified only if the overpass query 213 * is not restricted to bbox. 214 */ 215 if (!settings.getDownloadBounds().isPresent() && query.contains("{{bbox}}")) { 216 JOptionPane.showMessageDialog( 217 this.getParent(), 218 tr("Please select a download area first."), 219 tr("Error"), 220 JOptionPane.ERROR_MESSAGE 221 ); 222 return false; 223 } 224 225 /* 226 * Check for an empty query. User might want to download everything, if so validation is passed, 227 * otherwise return false. 228 */ 229 if (query.matches("(/\\*(\\*[^/]|[^\\*/])*\\*/|\\s)*")) { 230 boolean doFix = ConditionalOptionPaneUtil.showConfirmationDialog( 231 "download.overpass.fix.emptytoall", 232 this, 233 tr("You entered an empty query. Do you want to download all data in this area instead?"), 234 tr("Download all data?"), 235 JOptionPane.YES_NO_OPTION, 236 JOptionPane.QUESTION_MESSAGE, 237 JOptionPane.YES_OPTION); 238 if (doFix) { 239 String repairedQuery = "[out:xml]; \n" 240 + query + "\n" 241 + "(\n" 242 + " node({{bbox}});\n" 243 + "<;\n" 244 + ");\n" 245 + "(._;>;);" 246 + "out meta;"; 247 this.overpassQuery.setText(repairedQuery); 248 } else { 249 return false; 250 } 251 } 252 253 return true; 254 } 255 256 /** 257 * Sets query to the query text field. 258 * @param query The query to set. 259 */ 260 public void setOverpassQuery(String 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); 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 364 /** 365 * Encapsulates data that is required to preform download from Overpass API. 366 */ 367 static class OverpassDownloadData { 368 private final String query; 369 private final Consumer<Collection<Object>> errorReporter; 370 371 OverpassDownloadData(String query, Consumer<Collection<Object>> errorReporter) { 372 this.query = query; 373 this.errorReporter = errorReporter; 374 } 375 376 String getQuery() { 377 return this.query; 378 } 379 380 Consumer<Collection<Object>> getErrorReporter() { 381 return this.errorReporter; 382 } 383 } 384}