001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.BorderLayout; 009import java.awt.Component; 010import java.awt.Dimension; 011import java.awt.FlowLayout; 012import java.awt.GridBagLayout; 013import java.awt.Image; 014import java.awt.event.ActionEvent; 015import java.awt.event.KeyEvent; 016import java.awt.event.WindowAdapter; 017import java.awt.event.WindowEvent; 018import java.beans.PropertyChangeEvent; 019import java.beans.PropertyChangeListener; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026 027import javax.swing.AbstractAction; 028import javax.swing.BorderFactory; 029import javax.swing.Icon; 030import javax.swing.ImageIcon; 031import javax.swing.JButton; 032import javax.swing.JComponent; 033import javax.swing.JOptionPane; 034import javax.swing.JPanel; 035import javax.swing.JTabbedPane; 036import javax.swing.KeyStroke; 037 038import org.openstreetmap.josm.Main; 039import org.openstreetmap.josm.data.APIDataSet; 040import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 041import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 042import org.openstreetmap.josm.data.Preferences.Setting; 043import org.openstreetmap.josm.data.osm.Changeset; 044import org.openstreetmap.josm.data.osm.OsmPrimitive; 045import org.openstreetmap.josm.gui.ExtendedDialog; 046import org.openstreetmap.josm.gui.HelpAwareOptionPane; 047import org.openstreetmap.josm.gui.SideButton; 048import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 049import org.openstreetmap.josm.gui.help.HelpUtil; 050import org.openstreetmap.josm.io.OsmApi; 051import org.openstreetmap.josm.tools.GBC; 052import org.openstreetmap.josm.tools.ImageProvider; 053import org.openstreetmap.josm.tools.InputMapUtils; 054import org.openstreetmap.josm.tools.Utils; 055import org.openstreetmap.josm.tools.WindowGeometry; 056 057/** 058 * This is a dialog for entering upload options like the parameters for 059 * the upload changeset and the strategy for opening/closing a changeset. 060 * @since 2025 061 */ 062public class UploadDialog extends AbstractUploadDialog implements PropertyChangeListener, PreferenceChangedListener { 063 /** the unique instance of the upload dialog */ 064 private static UploadDialog uploadDialog; 065 066 /** 067 * List of custom components that can be added by plugins at JOSM startup. 068 */ 069 private static final Collection<Component> customComponents = new ArrayList<>(); 070 071 /** 072 * Replies the unique instance of the upload dialog 073 * 074 * @return the unique instance of the upload dialog 075 */ 076 public static UploadDialog getUploadDialog() { 077 if (uploadDialog == null) { 078 uploadDialog = new UploadDialog(); 079 } 080 return uploadDialog; 081 } 082 083 /** the panel with the objects to upload */ 084 private UploadedObjectsSummaryPanel pnlUploadedObjects; 085 /** the panel to select the changeset used */ 086 private ChangesetManagementPanel pnlChangesetManagement; 087 088 private BasicUploadSettingsPanel pnlBasicUploadSettings; 089 090 private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel; 091 092 /** checkbox for selecting whether an atomic upload is to be used */ 093 private TagSettingsPanel pnlTagSettings; 094 /** the tabbed pane used below of the list of primitives */ 095 private JTabbedPane tpConfigPanels; 096 /** the upload button */ 097 private JButton btnUpload; 098 099 /** the changeset comment model keeping the state of the changeset comment */ 100 private final ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel(); 101 private final ChangesetCommentModel changesetSourceModel = new ChangesetCommentModel(); 102 103 /** 104 * builds the content panel for the upload dialog 105 * 106 * @return the content panel 107 */ 108 protected JPanel buildContentPanel() { 109 JPanel pnl = new JPanel(new GridBagLayout()); 110 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 111 112 // the panel with the list of uploaded objects 113 // 114 pnl.add(pnlUploadedObjects = new UploadedObjectsSummaryPanel(), GBC.eol().fill(GBC.BOTH)); 115 116 // Custom components 117 for (Component c : customComponents) { 118 pnl.add(c, GBC.eol().fill(GBC.HORIZONTAL)); 119 } 120 121 // a tabbed pane with configuration panels in the lower half 122 // 123 tpConfigPanels = new JTabbedPane() { 124 @Override 125 public Dimension getPreferredSize() { 126 // make sure the tabbed pane never grabs more space than necessary 127 // 128 return super.getMinimumSize(); 129 } 130 }; 131 132 tpConfigPanels.add(pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel, changesetSourceModel)); 133 tpConfigPanels.setTitleAt(0, tr("Settings")); 134 tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use")); 135 136 tpConfigPanels.add(pnlTagSettings = new TagSettingsPanel(changesetCommentModel, changesetSourceModel)); 137 tpConfigPanels.setTitleAt(1, tr("Tags of new changeset")); 138 tpConfigPanels.setToolTipTextAt(1, tr("Apply tags to the changeset data is uploaded to")); 139 140 tpConfigPanels.add(pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel)); 141 tpConfigPanels.setTitleAt(2, tr("Changesets")); 142 tpConfigPanels.setToolTipTextAt(2, tr("Manage open changesets and select a changeset to upload to")); 143 144 tpConfigPanels.add(pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel()); 145 tpConfigPanels.setTitleAt(3, tr("Advanced")); 146 tpConfigPanels.setToolTipTextAt(3, tr("Configure advanced settings")); 147 148 pnl.add(tpConfigPanels, GBC.eol().fill(GBC.HORIZONTAL)); 149 return pnl; 150 } 151 152 /** 153 * builds the panel with the OK and CANCEL buttons 154 * 155 * @return The panel with the OK and CANCEL buttons 156 */ 157 protected JPanel buildActionPanel() { 158 JPanel pnl = new JPanel(); 159 pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); 160 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 161 162 // -- upload button 163 UploadAction uploadAction = new UploadAction(); 164 pnl.add(btnUpload = new SideButton(uploadAction)); 165 btnUpload.setFocusable(true); 166 InputMapUtils.enableEnter(btnUpload); 167 168 // -- cancel button 169 CancelAction cancelAction = new CancelAction(); 170 pnl.add(new SideButton(cancelAction)); 171 getRootPane().registerKeyboardAction( 172 cancelAction, 173 KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), 174 JComponent.WHEN_IN_FOCUSED_WINDOW 175 ); 176 pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload")))); 177 HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/Upload")); 178 return pnl; 179 } 180 181 /** 182 * builds the gui 183 */ 184 protected void build() { 185 setTitle(tr("Upload to ''{0}''", OsmApi.getOsmApi().getBaseUrl())); 186 getContentPane().setLayout(new BorderLayout()); 187 getContentPane().add(buildContentPanel(), BorderLayout.CENTER); 188 getContentPane().add(buildActionPanel(), BorderLayout.SOUTH); 189 190 addWindowListener(new WindowEventHandler()); 191 192 193 // make sure the configuration panels listen to each other 194 // changes 195 // 196 pnlChangesetManagement.addPropertyChangeListener( 197 pnlBasicUploadSettings.getUploadParameterSummaryPanel() 198 ); 199 pnlChangesetManagement.addPropertyChangeListener(this); 200 pnlUploadedObjects.addPropertyChangeListener( 201 pnlBasicUploadSettings.getUploadParameterSummaryPanel() 202 ); 203 pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel); 204 pnlUploadStrategySelectionPanel.addPropertyChangeListener( 205 pnlBasicUploadSettings.getUploadParameterSummaryPanel() 206 ); 207 208 209 // users can click on either of two links in the upload parameter 210 // summary handler. This installs the handler for these two events. 211 // We simply select the appropriate tab in the tabbed pane with the 212 // configuration dialogs. 213 // 214 pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener( 215 new ConfigurationParameterRequestHandler() { 216 @Override 217 public void handleUploadStrategyConfigurationRequest() { 218 tpConfigPanels.setSelectedIndex(3); 219 } 220 @Override 221 public void handleChangesetConfigurationRequest() { 222 tpConfigPanels.setSelectedIndex(2); 223 } 224 } 225 ); 226 227 pnlBasicUploadSettings.setUploadTagDownFocusTraversalHandlers( 228 new AbstractAction() { 229 @Override 230 public void actionPerformed(ActionEvent e) { 231 btnUpload.requestFocusInWindow(); 232 } 233 } 234 ); 235 236 setMinimumSize(new Dimension(300, 350)); 237 238 Main.pref.addPreferenceChangeListener(this); 239 } 240 241 /** 242 * constructor 243 */ 244 public UploadDialog() { 245 super(JOptionPane.getFrameForComponent(Main.parent), ModalityType.DOCUMENT_MODAL); 246 build(); 247 } 248 249 /** 250 * Sets the collection of primitives to upload 251 * 252 * @param toUpload the dataset with the objects to upload. If null, assumes the empty 253 * set of objects to upload 254 * 255 */ 256 public void setUploadedPrimitives(APIDataSet toUpload) { 257 if (toUpload == null) { 258 List<OsmPrimitive> emptyList = Collections.emptyList(); 259 pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList); 260 return; 261 } 262 pnlUploadedObjects.setUploadedPrimitives( 263 toUpload.getPrimitivesToAdd(), 264 toUpload.getPrimitivesToUpdate(), 265 toUpload.getPrimitivesToDelete() 266 ); 267 } 268 269 @Override 270 public void rememberUserInput() { 271 pnlBasicUploadSettings.rememberUserInput(); 272 pnlUploadStrategySelectionPanel.rememberUserInput(); 273 } 274 275 /** 276 * Initializes the panel for user input 277 */ 278 public void startUserInput() { 279 tpConfigPanels.setSelectedIndex(0); 280 pnlBasicUploadSettings.startUserInput(); 281 pnlTagSettings.startUserInput(); 282 pnlTagSettings.initFromChangeset(pnlChangesetManagement.getSelectedChangeset()); 283 pnlUploadStrategySelectionPanel.initFromPreferences(); 284 UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel(); 285 pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification()); 286 pnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload()); 287 pnl.setNumObjects(pnlUploadedObjects.getNumObjectsToUpload()); 288 } 289 290 /** 291 * Replies the current changeset 292 * 293 * @return the current changeset 294 */ 295 public Changeset getChangeset() { 296 Changeset cs = pnlChangesetManagement.getSelectedChangeset(); 297 if (cs == null) { 298 cs = new Changeset(); 299 } 300 cs.setKeys(pnlTagSettings.getTags(false)); 301 return cs; 302 } 303 304 public void setSelectedChangesetForNextUpload(Changeset cs) { 305 pnlChangesetManagement.setSelectedChangesetForNextUpload(cs); 306 } 307 308 public Map<String, String> getDefaultChangesetTags() { 309 return pnlTagSettings.getDefaultTags(); 310 } 311 312 public void setDefaultChangesetTags(Map<String, String> tags) { 313 pnlTagSettings.setDefaultTags(tags); 314 changesetCommentModel.setComment(tags.get("comment")); 315 changesetSourceModel.setComment(tags.get("source")); 316 } 317 318 /** 319 * Replies the {@link UploadStrategySpecification} the user entered in the dialog. 320 * 321 * @return the {@link UploadStrategySpecification} the user entered in the dialog. 322 */ 323 public UploadStrategySpecification getUploadStrategySpecification() { 324 UploadStrategySpecification spec = pnlUploadStrategySelectionPanel.getUploadStrategySpecification(); 325 spec.setCloseChangesetAfterUpload(pnlChangesetManagement.isCloseChangesetAfterUpload()); 326 return spec; 327 } 328 329 /** 330 * Returns the current value for the upload comment 331 * 332 * @return the current value for the upload comment 333 */ 334 protected String getUploadComment() { 335 return changesetCommentModel.getComment(); 336 } 337 338 /** 339 * Returns the current value for the changeset source 340 * 341 * @return the current value for the changeset source 342 */ 343 protected String getUploadSource() { 344 return changesetSourceModel.getComment(); 345 } 346 347 @Override 348 public void setVisible(boolean visible) { 349 if (visible) { 350 new WindowGeometry( 351 getClass().getName() + ".geometry", 352 WindowGeometry.centerInWindow( 353 Main.parent, 354 new Dimension(400,600) 355 ) 356 ).applySafe(this); 357 startUserInput(); 358 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 359 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 360 } 361 super.setVisible(visible); 362 } 363 364 /** 365 * Adds a custom component to this dialog. 366 * Custom components added at JOSM startup are displayed between the objects list and the properties tab pane. 367 * @param c The custom component to add. If {@code null}, this method does nothing. 368 * @return {@code true} if the collection of custom components changed as a result of the call 369 * @since 5842 370 */ 371 public static boolean addCustomComponent(Component c) { 372 if (c != null) { 373 return customComponents.add(c); 374 } 375 return false; 376 } 377 378 /** 379 * Handles an upload 380 * 381 */ 382 class UploadAction extends AbstractAction { 383 public UploadAction() { 384 putValue(NAME, tr("Upload Changes")); 385 putValue(SMALL_ICON, ImageProvider.get("upload")); 386 putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives")); 387 } 388 389 /** 390 * Displays a warning message indicating that the upload comment is empty/short. 391 * @return true if the user wants to revisit, false if they want to continue 392 */ 393 protected boolean warnUploadComment() { 394 return warnUploadTag( 395 tr("Please revise upload comment"), 396 tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" + 397 "This is technically allowed, but please consider that many users who are<br />" + 398 "watching changes in their area depend on meaningful changeset comments<br />" + 399 "to understand what is going on!<br /><br />" + 400 "If you spend a minute now to explain your change, you will make life<br />" + 401 "easier for many other mappers."), 402 "upload_comment_is_empty_or_very_short" 403 ); 404 } 405 406 /** 407 * Displays a warning message indicating that no changeset source is given. 408 * @return true if the user wants to revisit, false if they want to continue 409 */ 410 protected boolean warnUploadSource() { 411 return warnUploadTag( 412 tr("Please specify a changeset source"), 413 tr("You did not specify a source for your changes.<br />" + 414 "It is technically allowed, but this information helps<br />" + 415 "other users to understand the origins of the data.<br /><br />" + 416 "If you spend a minute now to explain your change, you will make life<br />" + 417 "easier for many other mappers."), 418 "upload_source_is_empty" 419 ); 420 } 421 422 protected boolean warnUploadTag(final String title, final String message, final String togglePref) { 423 ExtendedDialog dlg = new ExtendedDialog(UploadDialog.this, 424 title, 425 new String[] {tr("Revise"), tr("Cancel"), tr("Continue as is")}); 426 dlg.setContent("<html>" + message + "</html>"); 427 dlg.setButtonIcons(new Icon[] { 428 ImageProvider.get("ok"), 429 ImageProvider.get("cancel"), 430 ImageProvider.overlay( 431 ImageProvider.get("upload"), 432 new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)), 433 ImageProvider.OverlayPosition.SOUTHEAST)}); 434 dlg.setToolTipTexts(new String[] { 435 tr("Return to the previous dialog to enter a more descriptive comment"), 436 tr("Cancel and return to the previous dialog"), 437 tr("Ignore this hint and upload anyway")}); 438 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 439 dlg.toggleEnable(togglePref); 440 dlg.setCancelButton(1, 2); 441 return dlg.showDialog().getValue() != 3; 442 } 443 444 protected void warnIllegalChunkSize() { 445 HelpAwareOptionPane.showOptionDialog( 446 UploadDialog.this, 447 tr("Please enter a valid chunk size first"), 448 tr("Illegal chunk size"), 449 JOptionPane.ERROR_MESSAGE, 450 ht("/Dialog/Upload#IllegalChunkSize") 451 ); 452 } 453 454 @Override 455 public void actionPerformed(ActionEvent e) { 456 if ((getUploadComment().trim().length() < 10 && warnUploadComment()) /* abort for missing comment */ 457 || (getUploadSource().trim().isEmpty() && warnUploadSource()) /* abort for missing changeset source */ 458 ) { 459 tpConfigPanels.setSelectedIndex(0); 460 pnlBasicUploadSettings.initEditingOfUploadComment(); 461 return; 462 } 463 464 /* test for empty tags in the changeset metadata and proceed only after user's confirmation. 465 * though, accept if key and value are empty (cf. xor). */ 466 List<String> emptyChangesetTags = new ArrayList<>(); 467 for (final Entry<String, String> i : pnlTagSettings.getTags(true).entrySet()) { 468 final boolean isKeyEmpty = i.getKey() == null || i.getKey().trim().isEmpty(); 469 final boolean isValueEmpty = i.getValue() == null || i.getValue().trim().isEmpty(); 470 final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey()); 471 if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) { 472 emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue())); 473 } 474 } 475 if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog( 476 Main.parent, 477 trn( 478 "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>", 479 "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>", 480 emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)), 481 tr("Empty metadata"), 482 JOptionPane.OK_CANCEL_OPTION, 483 JOptionPane.WARNING_MESSAGE 484 )) { 485 tpConfigPanels.setSelectedIndex(0); 486 pnlBasicUploadSettings.initEditingOfUploadComment(); 487 return; 488 } 489 490 UploadStrategySpecification strategy = getUploadStrategySpecification(); 491 if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)) { 492 if (strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 493 warnIllegalChunkSize(); 494 tpConfigPanels.setSelectedIndex(0); 495 return; 496 } 497 } 498 setCanceled(false); 499 setVisible(false); 500 } 501 } 502 503 /** 504 * Action for canceling the dialog 505 * 506 */ 507 class CancelAction extends AbstractAction { 508 public CancelAction() { 509 putValue(NAME, tr("Cancel")); 510 putValue(SMALL_ICON, ImageProvider.get("cancel")); 511 putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing")); 512 } 513 514 @Override 515 public void actionPerformed(ActionEvent e) { 516 setCanceled(true); 517 setVisible(false); 518 } 519 } 520 521 /** 522 * Listens to window closing events and processes them as cancel events. 523 * Listens to window open events and initializes user input 524 * 525 */ 526 class WindowEventHandler extends WindowAdapter { 527 @Override 528 public void windowClosing(WindowEvent e) { 529 setCanceled(true); 530 } 531 532 @Override 533 public void windowActivated(WindowEvent arg0) { 534 if (tpConfigPanels.getSelectedIndex() == 0) { 535 pnlBasicUploadSettings.initEditingOfUploadComment(); 536 } 537 } 538 } 539 540 /* -------------------------------------------------------------------------- */ 541 /* Interface PropertyChangeListener */ 542 /* -------------------------------------------------------------------------- */ 543 @Override 544 public void propertyChange(PropertyChangeEvent evt) { 545 if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) { 546 Changeset cs = (Changeset)evt.getNewValue(); 547 if (cs == null) { 548 tpConfigPanels.setTitleAt(1, tr("Tags of new changeset")); 549 } else { 550 tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId())); 551 } 552 } 553 } 554 555 /* -------------------------------------------------------------------------- */ 556 /* Interface PreferenceChangedListener */ 557 /* -------------------------------------------------------------------------- */ 558 @Override 559 public void preferenceChanged(PreferenceChangeEvent e) { 560 if (e.getKey() == null || !"osm-server.url".equals(e.getKey())) 561 return; 562 final Setting<?> newValue = e.getNewValue(); 563 final String url; 564 if (newValue == null || newValue.getValue() == null) { 565 url = OsmApi.getOsmApi().getBaseUrl(); 566 } else { 567 url = newValue.getValue().toString(); 568 } 569 setTitle(tr("Upload to ''{0}''", url)); 570 } 571 572 private String getLastChangesetTagFromHistory(String historyKey) { 573 Collection<String> history = Main.pref.getCollection(historyKey, new ArrayList<String>()); 574 int age = (int) (System.currentTimeMillis() / 1000 - Main.pref.getInteger(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0)); 575 if (age < Main.pref.getInteger(BasicUploadSettingsPanel.HISTORY_MAX_AGE_KEY, 4 * 3600 * 1000) && history != null && !history.isEmpty()) { 576 return history.iterator().next(); 577 } else { 578 return null; 579 } 580 } 581 582 public String getLastChangesetCommentFromHistory() { 583 return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.HISTORY_KEY); 584 } 585 586 public String getLastChangesetSourceFromHistory() { 587 return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY); 588 } 589}