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.event.ActionEvent; 014import java.awt.event.KeyEvent; 015import java.awt.event.WindowAdapter; 016import java.awt.event.WindowEvent; 017import java.beans.PropertyChangeEvent; 018import java.beans.PropertyChangeListener; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.List; 023import java.util.Map; 024import java.util.Map.Entry; 025 026import javax.swing.AbstractAction; 027import javax.swing.BorderFactory; 028import javax.swing.Icon; 029import javax.swing.JButton; 030import javax.swing.JComponent; 031import javax.swing.JOptionPane; 032import javax.swing.JPanel; 033import javax.swing.JTabbedPane; 034import javax.swing.KeyStroke; 035 036import org.openstreetmap.josm.Main; 037import org.openstreetmap.josm.data.APIDataSet; 038import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 039import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 040import org.openstreetmap.josm.data.Preferences.Setting; 041import org.openstreetmap.josm.data.osm.Changeset; 042import org.openstreetmap.josm.data.osm.OsmPrimitive; 043import org.openstreetmap.josm.gui.ExtendedDialog; 044import org.openstreetmap.josm.gui.HelpAwareOptionPane; 045import org.openstreetmap.josm.gui.SideButton; 046import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 047import org.openstreetmap.josm.gui.help.HelpUtil; 048import org.openstreetmap.josm.io.OsmApi; 049import org.openstreetmap.josm.tools.GBC; 050import org.openstreetmap.josm.tools.ImageOverlay; 051import org.openstreetmap.josm.tools.ImageProvider; 052import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 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 synchronized 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 transient ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel(); 101 private final transient 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 configuration dialogs. 212 // 213 pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener( 214 new ConfigurationParameterRequestHandler() { 215 @Override 216 public void handleUploadStrategyConfigurationRequest() { 217 tpConfigPanels.setSelectedIndex(3); 218 } 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 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 new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).get(), 429 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(), 430 new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( 431 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()}); 432 dlg.setToolTipTexts(new String[] { 433 tr("Return to the previous dialog to enter a more descriptive comment"), 434 tr("Cancel and return to the previous dialog"), 435 tr("Ignore this hint and upload anyway")}); 436 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 437 dlg.toggleEnable(togglePref); 438 dlg.setCancelButton(1, 2); 439 return dlg.showDialog().getValue() != 3; 440 } 441 442 protected void warnIllegalChunkSize() { 443 HelpAwareOptionPane.showOptionDialog( 444 UploadDialog.this, 445 tr("Please enter a valid chunk size first"), 446 tr("Illegal chunk size"), 447 JOptionPane.ERROR_MESSAGE, 448 ht("/Dialog/Upload#IllegalChunkSize") 449 ); 450 } 451 452 @Override 453 public void actionPerformed(ActionEvent e) { 454 if ((getUploadComment().trim().length() < 10 && warnUploadComment()) /* abort for missing comment */ 455 || (getUploadSource().trim().isEmpty() && warnUploadSource()) /* abort for missing changeset source */ 456 ) { 457 tpConfigPanels.setSelectedIndex(0); 458 pnlBasicUploadSettings.initEditingOfUploadComment(); 459 return; 460 } 461 462 /* test for empty tags in the changeset metadata and proceed only after user's confirmation. 463 * though, accept if key and value are empty (cf. xor). */ 464 List<String> emptyChangesetTags = new ArrayList<>(); 465 for (final Entry<String, String> i : pnlTagSettings.getTags(true).entrySet()) { 466 final boolean isKeyEmpty = i.getKey() == null || i.getKey().trim().isEmpty(); 467 final boolean isValueEmpty = i.getValue() == null || i.getValue().trim().isEmpty(); 468 final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey()); 469 if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) { 470 emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue())); 471 } 472 } 473 if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog( 474 Main.parent, 475 trn( 476 "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>", 477 "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>", 478 emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)), 479 tr("Empty metadata"), 480 JOptionPane.OK_CANCEL_OPTION, 481 JOptionPane.WARNING_MESSAGE 482 )) { 483 tpConfigPanels.setSelectedIndex(0); 484 pnlBasicUploadSettings.initEditingOfUploadComment(); 485 return; 486 } 487 488 UploadStrategySpecification strategy = getUploadStrategySpecification(); 489 if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)) { 490 if (strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 491 warnIllegalChunkSize(); 492 tpConfigPanels.setSelectedIndex(0); 493 return; 494 } 495 } 496 setCanceled(false); 497 setVisible(false); 498 } 499 } 500 501 /** 502 * Action for canceling the dialog 503 * 504 */ 505 class CancelAction extends AbstractAction { 506 CancelAction() { 507 putValue(NAME, tr("Cancel")); 508 putValue(SMALL_ICON, ImageProvider.get("cancel")); 509 putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing")); 510 } 511 512 @Override 513 public void actionPerformed(ActionEvent e) { 514 setCanceled(true); 515 setVisible(false); 516 } 517 } 518 519 /** 520 * Listens to window closing events and processes them as cancel events. 521 * Listens to window open events and initializes user input 522 * 523 */ 524 class WindowEventHandler extends WindowAdapter { 525 @Override 526 public void windowClosing(WindowEvent e) { 527 setCanceled(true); 528 } 529 530 @Override 531 public void windowActivated(WindowEvent arg0) { 532 if (tpConfigPanels.getSelectedIndex() == 0) { 533 pnlBasicUploadSettings.initEditingOfUploadComment(); 534 } 535 } 536 } 537 538 /* -------------------------------------------------------------------------- */ 539 /* Interface PropertyChangeListener */ 540 /* -------------------------------------------------------------------------- */ 541 @Override 542 public void propertyChange(PropertyChangeEvent evt) { 543 if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) { 544 Changeset cs = (Changeset) evt.getNewValue(); 545 if (cs == null) { 546 tpConfigPanels.setTitleAt(1, tr("Tags of new changeset")); 547 } else { 548 tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId())); 549 } 550 } 551 } 552 553 /* -------------------------------------------------------------------------- */ 554 /* Interface PreferenceChangedListener */ 555 /* -------------------------------------------------------------------------- */ 556 @Override 557 public void preferenceChanged(PreferenceChangeEvent e) { 558 if (e.getKey() == null || !"osm-server.url".equals(e.getKey())) 559 return; 560 final Setting<?> newValue = e.getNewValue(); 561 final String url; 562 if (newValue == null || newValue.getValue() == null) { 563 url = OsmApi.getOsmApi().getBaseUrl(); 564 } else { 565 url = newValue.getValue().toString(); 566 } 567 setTitle(tr("Upload to ''{0}''", url)); 568 } 569 570 private static String getLastChangesetTagFromHistory(String historyKey, List<String> def) { 571 Collection<String> history = Main.pref.getCollection(historyKey, def); 572 int age = (int) (System.currentTimeMillis() / 1000 - Main.pref.getInteger(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0)); 573 if (age < Main.pref.getInteger(BasicUploadSettingsPanel.HISTORY_MAX_AGE_KEY, 4 * 3600 * 1000) && history != null && !history.isEmpty()) { 574 return history.iterator().next(); 575 } else { 576 return null; 577 } 578 } 579 580 public String getLastChangesetCommentFromHistory() { 581 return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.HISTORY_KEY, new ArrayList<String>()); 582 } 583 584 public String getLastChangesetSourceFromHistory() { 585 return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY, BasicUploadSettingsPanel.getDefaultSources()); 586 } 587}