001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.Color; 008import java.awt.Component; 009import java.awt.GridBagConstraints; 010import java.awt.GridBagLayout; 011import java.awt.Insets; 012import java.awt.event.ActionEvent; 013import java.awt.event.ActionListener; 014import java.awt.event.FocusAdapter; 015import java.awt.event.FocusEvent; 016import java.awt.event.ItemEvent; 017import java.awt.event.ItemListener; 018import java.beans.PropertyChangeEvent; 019import java.beans.PropertyChangeListener; 020import java.util.EnumMap; 021import java.util.Map; 022import java.util.Map.Entry; 023 024import javax.swing.BorderFactory; 025import javax.swing.ButtonGroup; 026import javax.swing.JLabel; 027import javax.swing.JPanel; 028import javax.swing.JRadioButton; 029import javax.swing.UIManager; 030import javax.swing.event.DocumentEvent; 031import javax.swing.event.DocumentListener; 032 033import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 034import org.openstreetmap.josm.gui.widgets.JosmTextField; 035import org.openstreetmap.josm.io.Capabilities; 036import org.openstreetmap.josm.io.MaxChangesetSizeExceededPolicy; 037import org.openstreetmap.josm.io.OsmApi; 038import org.openstreetmap.josm.io.UploadStrategy; 039import org.openstreetmap.josm.io.UploadStrategySpecification; 040import org.openstreetmap.josm.spi.preferences.Config; 041import org.openstreetmap.josm.tools.Logging; 042 043/** 044 * UploadStrategySelectionPanel is a panel for selecting an upload strategy. 045 * 046 * Clients can listen for property change events for the property 047 * {@link #UPLOAD_STRATEGY_SPECIFICATION_PROP}. 048 */ 049public class UploadStrategySelectionPanel extends JPanel implements PropertyChangeListener { 050 051 /** 052 * The property for the upload strategy 053 */ 054 public static final String UPLOAD_STRATEGY_SPECIFICATION_PROP = 055 UploadStrategySelectionPanel.class.getName() + ".uploadStrategySpecification"; 056 057 private static final Color BG_COLOR_ERROR = new Color(255, 224, 224); 058 059 private transient Map<UploadStrategy, JRadioButton> rbStrategy; 060 private transient Map<UploadStrategy, JLabel> lblNumRequests; 061 private transient Map<UploadStrategy, JMultilineLabel> lblStrategies; 062 private final JosmTextField tfChunkSize = new JosmTextField(4); 063 private final JPanel pnlMultiChangesetPolicyPanel = new JPanel(new GridBagLayout()); 064 private final JRadioButton rbFillOneChangeset = new JRadioButton( 065 tr("Fill up one changeset and return to the Upload Dialog")); 066 private final JRadioButton rbUseMultipleChangesets = new JRadioButton( 067 tr("Open and use as many new changesets as necessary")); 068 private JMultilineLabel lblMultiChangesetPoliciesHeader; 069 070 private long numUploadedObjects; 071 072 /** 073 * Constructs a new {@code UploadStrategySelectionPanel}. 074 */ 075 public UploadStrategySelectionPanel() { 076 build(); 077 } 078 079 protected JPanel buildUploadStrategyPanel() { 080 JPanel pnl = new JPanel(new GridBagLayout()); 081 ButtonGroup bgStrategies = new ButtonGroup(); 082 rbStrategy = new EnumMap<>(UploadStrategy.class); 083 lblStrategies = new EnumMap<>(UploadStrategy.class); 084 lblNumRequests = new EnumMap<>(UploadStrategy.class); 085 for (UploadStrategy strategy: UploadStrategy.values()) { 086 rbStrategy.put(strategy, new JRadioButton()); 087 lblNumRequests.put(strategy, new JLabel()); 088 lblStrategies.put(strategy, new JMultilineLabel("")); 089 bgStrategies.add(rbStrategy.get(strategy)); 090 } 091 092 // -- headline 093 GridBagConstraints gc = new GridBagConstraints(); 094 gc.gridx = 0; 095 gc.gridy = 0; 096 gc.weightx = 1.0; 097 gc.weighty = 0.0; 098 gc.gridwidth = 4; 099 gc.fill = GridBagConstraints.HORIZONTAL; 100 gc.insets = new Insets(0, 0, 3, 0); 101 gc.anchor = GridBagConstraints.FIRST_LINE_START; 102 pnl.add(new JMultilineLabel(tr("Please select the upload strategy:")), gc); 103 104 // -- single request strategy 105 gc.gridx = 0; 106 gc.gridy = 1; 107 gc.weightx = 0.0; 108 gc.weighty = 0.0; 109 gc.gridwidth = 1; 110 gc.anchor = GridBagConstraints.FIRST_LINE_START; 111 pnl.add(rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY), gc); 112 gc.gridx = 1; 113 gc.gridy = 1; 114 gc.weightx = 1.0; 115 gc.weighty = 0.0; 116 gc.gridwidth = 2; 117 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 118 lbl.setText(tr("Upload data in one request")); 119 pnl.add(lbl, gc); 120 gc.gridx = 3; 121 gc.gridy = 1; 122 gc.weightx = 0.0; 123 gc.weighty = 0.0; 124 gc.gridwidth = 1; 125 pnl.add(lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY), gc); 126 127 // -- chunked dataset strategy 128 gc.gridx = 0; 129 gc.gridy = 2; 130 gc.weightx = 0.0; 131 gc.weighty = 0.0; 132 pnl.add(rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY), gc); 133 gc.gridx = 1; 134 gc.gridy = 2; 135 gc.weightx = 1.0; 136 gc.weighty = 0.0; 137 gc.gridwidth = 1; 138 lbl = lblStrategies.get(UploadStrategy.CHUNKED_DATASET_STRATEGY); 139 lbl.setText(tr("Upload data in chunks of objects. Chunk size: ")); 140 pnl.add(lbl, gc); 141 gc.gridx = 2; 142 gc.gridy = 2; 143 gc.weightx = 0.0; 144 gc.weighty = 0.0; 145 gc.gridwidth = 1; 146 pnl.add(tfChunkSize, gc); 147 gc.gridx = 3; 148 gc.gridy = 2; 149 gc.weightx = 0.0; 150 gc.weighty = 0.0; 151 gc.gridwidth = 1; 152 pnl.add(lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY), gc); 153 154 // -- single request strategy 155 gc.gridx = 0; 156 gc.gridy = 3; 157 gc.weightx = 0.0; 158 gc.weighty = 0.0; 159 pnl.add(rbStrategy.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY), gc); 160 gc.gridx = 1; 161 gc.gridy = 3; 162 gc.weightx = 1.0; 163 gc.weighty = 0.0; 164 gc.gridwidth = 2; 165 lbl = lblStrategies.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY); 166 lbl.setText(tr("Upload each object individually")); 167 pnl.add(lbl, gc); 168 gc.gridx = 3; 169 gc.gridy = 3; 170 gc.weightx = 0.0; 171 gc.weighty = 0.0; 172 gc.gridwidth = 1; 173 pnl.add(lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY), gc); 174 175 tfChunkSize.addFocusListener(new TextFieldFocusHandler()); 176 tfChunkSize.getDocument().addDocumentListener(new ChunkSizeInputVerifier()); 177 178 StrategyChangeListener strategyChangeListener = new StrategyChangeListener(); 179 tfChunkSize.addFocusListener(strategyChangeListener); 180 tfChunkSize.addActionListener(strategyChangeListener); 181 for (UploadStrategy strategy: UploadStrategy.values()) { 182 rbStrategy.get(strategy).addItemListener(strategyChangeListener); 183 } 184 185 return pnl; 186 } 187 188 protected JPanel buildMultiChangesetPolicyPanel() { 189 GridBagConstraints gc = new GridBagConstraints(); 190 gc.gridx = 0; 191 gc.gridy = 0; 192 gc.fill = GridBagConstraints.HORIZONTAL; 193 gc.anchor = GridBagConstraints.FIRST_LINE_START; 194 gc.weightx = 1.0; 195 lblMultiChangesetPoliciesHeader = new JMultilineLabel( 196 tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " + 197 "Which strategy do you want to use?</html>", 198 numUploadedObjects)); 199 pnlMultiChangesetPolicyPanel.add(lblMultiChangesetPoliciesHeader, gc); 200 gc.gridy = 1; 201 pnlMultiChangesetPolicyPanel.add(rbFillOneChangeset, gc); 202 gc.gridy = 2; 203 pnlMultiChangesetPolicyPanel.add(rbUseMultipleChangesets, gc); 204 205 ButtonGroup bgMultiChangesetPolicies = new ButtonGroup(); 206 bgMultiChangesetPolicies.add(rbFillOneChangeset); 207 bgMultiChangesetPolicies.add(rbUseMultipleChangesets); 208 return pnlMultiChangesetPolicyPanel; 209 } 210 211 protected void build() { 212 setLayout(new GridBagLayout()); 213 GridBagConstraints gc = new GridBagConstraints(); 214 gc.gridx = 0; 215 gc.gridy = 0; 216 gc.fill = GridBagConstraints.HORIZONTAL; 217 gc.weightx = 1.0; 218 gc.weighty = 0.0; 219 gc.anchor = GridBagConstraints.NORTHWEST; 220 gc.insets = new Insets(3, 3, 3, 3); 221 222 add(buildUploadStrategyPanel(), gc); 223 gc.gridy = 1; 224 add(buildMultiChangesetPolicyPanel(), gc); 225 226 // consume remaining space 227 gc.gridy = 2; 228 gc.fill = GridBagConstraints.BOTH; 229 gc.weightx = 1.0; 230 gc.weighty = 1.0; 231 add(new JPanel(), gc); 232 233 Capabilities capabilities = OsmApi.getOsmApi().getCapabilities(); 234 int maxChunkSize = capabilities != null ? capabilities.getMaxChangesetSize() : -1; 235 pnlMultiChangesetPolicyPanel.setVisible( 236 maxChunkSize > 0 && numUploadedObjects > maxChunkSize 237 ); 238 } 239 240 /** 241 * Sets the number of uploaded objects to display 242 * @param numUploadedObjects The number of objects 243 */ 244 public void setNumUploadedObjects(int numUploadedObjects) { 245 this.numUploadedObjects = Math.max(numUploadedObjects, 0); 246 updateNumRequestsLabels(); 247 } 248 249 /** 250 * Fills the inputs using a {@link UploadStrategySpecification} 251 * @param strategy The strategy 252 */ 253 public void setUploadStrategySpecification(UploadStrategySpecification strategy) { 254 if (strategy == null) 255 return; 256 rbStrategy.get(strategy.getStrategy()).setSelected(true); 257 tfChunkSize.setEnabled(strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY); 258 if (strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY) { 259 if (strategy.getChunkSize() != UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 260 tfChunkSize.setText(Integer.toString(strategy.getChunkSize())); 261 } else { 262 tfChunkSize.setText("1"); 263 } 264 } 265 } 266 267 /** 268 * Gets the upload strategy the user chose 269 * @return The strategy 270 */ 271 public UploadStrategySpecification getUploadStrategySpecification() { 272 UploadStrategy strategy = getUploadStrategy(); 273 UploadStrategySpecification spec = new UploadStrategySpecification(); 274 if (strategy != null) { 275 switch(strategy) { 276 case CHUNKED_DATASET_STRATEGY: 277 spec.setStrategy(strategy).setChunkSize(getChunkSize()); 278 break; 279 case INDIVIDUAL_OBJECTS_STRATEGY: 280 case SINGLE_REQUEST_STRATEGY: 281 default: 282 spec.setStrategy(strategy); 283 break; 284 } 285 } 286 if (pnlMultiChangesetPolicyPanel.isVisible()) { 287 if (rbFillOneChangeset.isSelected()) { 288 spec.setPolicy(MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG); 289 } else if (rbUseMultipleChangesets.isSelected()) { 290 spec.setPolicy(MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS); 291 } else { 292 spec.setPolicy(null); // unknown policy 293 } 294 } else { 295 spec.setPolicy(null); 296 } 297 return spec; 298 } 299 300 protected UploadStrategy getUploadStrategy() { 301 UploadStrategy strategy = null; 302 for (Entry<UploadStrategy, JRadioButton> e : rbStrategy.entrySet()) { 303 if (e.getValue().isSelected()) { 304 strategy = e.getKey(); 305 break; 306 } 307 } 308 return strategy; 309 } 310 311 protected int getChunkSize() { 312 try { 313 return Integer.parseInt(tfChunkSize.getText().trim()); 314 } catch (NumberFormatException e) { 315 return UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE; 316 } 317 } 318 319 /** 320 * Load the panel contents from preferences 321 */ 322 public void initFromPreferences() { 323 UploadStrategy strategy = UploadStrategy.getFromPreferences(); 324 rbStrategy.get(strategy).setSelected(true); 325 int chunkSize = Config.getPref().getInt("osm-server.upload-strategy.chunk-size", 1000); 326 tfChunkSize.setText(Integer.toString(chunkSize)); 327 updateNumRequestsLabels(); 328 } 329 330 /** 331 * Stores the values that the user has input into the preferences 332 */ 333 public void rememberUserInput() { 334 UploadStrategy strategy = getUploadStrategy(); 335 UploadStrategy.saveToPreferences(strategy); 336 int chunkSize; 337 try { 338 chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 339 Config.getPref().putInt("osm-server.upload-strategy.chunk-size", chunkSize); 340 } catch (NumberFormatException e) { 341 // don't save invalid value to preferences 342 Logging.trace(e); 343 } 344 } 345 346 protected void updateNumRequestsLabels() { 347 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 348 if (maxChunkSize > 0 && numUploadedObjects > maxChunkSize) { 349 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(false); 350 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 351 lbl.setText(tr("Upload in one request not possible (too many objects to upload)")); 352 lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>" 353 + "max. changeset size {1} on server ''{2}'' is exceeded.</html>", 354 numUploadedObjects, maxChunkSize, OsmApi.getOsmApi().getBaseUrl() 355 ) 356 ); 357 rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true); 358 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false); 359 360 lblMultiChangesetPoliciesHeader.setText( 361 tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " + 362 "Which strategy do you want to use?</html>", 363 numUploadedObjects)); 364 if (!rbFillOneChangeset.isSelected() && !rbUseMultipleChangesets.isSelected()) { 365 rbUseMultipleChangesets.setSelected(true); 366 } 367 pnlMultiChangesetPolicyPanel.setVisible(true); 368 369 } else { 370 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true); 371 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 372 lbl.setText(tr("Upload data in one request")); 373 lbl.setToolTipText(null); 374 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true); 375 376 pnlMultiChangesetPolicyPanel.setVisible(false); 377 } 378 379 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)")); 380 if (numUploadedObjects == 0) { 381 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)")); 382 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 383 } else { 384 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText( 385 trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects) 386 ); 387 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 388 int chunkSize = getChunkSize(); 389 if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 390 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 391 } else { 392 int chunks = (int) Math.ceil((double) numUploadedObjects / (double) chunkSize); 393 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText( 394 trn("({0} request)", "({0} requests)", chunks, chunks) 395 ); 396 } 397 } 398 } 399 400 /** 401 * Sets the focus on the chunk size field 402 */ 403 public void initEditingOfChunkSize() { 404 tfChunkSize.requestFocusInWindow(); 405 } 406 407 @Override 408 public void propertyChange(PropertyChangeEvent evt) { 409 if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) { 410 setNumUploadedObjects((Integer) evt.getNewValue()); 411 } 412 } 413 414 static class TextFieldFocusHandler extends FocusAdapter { 415 @Override 416 public void focusGained(FocusEvent e) { 417 Component c = e.getComponent(); 418 if (c instanceof JosmTextField) { 419 JosmTextField tf = (JosmTextField) c; 420 tf.selectAll(); 421 } 422 } 423 } 424 425 class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener { 426 protected void setErrorFeedback(JosmTextField tf, String message) { 427 tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); 428 tf.setToolTipText(message); 429 tf.setBackground(BG_COLOR_ERROR); 430 } 431 432 protected void clearErrorFeedback(JosmTextField tf, String message) { 433 tf.setBorder(UIManager.getBorder("TextField.border")); 434 tf.setToolTipText(message); 435 tf.setBackground(UIManager.getColor("TextField.background")); 436 } 437 438 protected void validateChunkSize() { 439 try { 440 int chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 441 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 442 if (chunkSize <= 0) { 443 setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1")); 444 } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 445 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", 446 chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 447 } else { 448 clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1")); 449 } 450 451 if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 452 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", 453 chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 454 } 455 } catch (NumberFormatException e) { 456 setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1", 457 tfChunkSize.getText().trim())); 458 } finally { 459 updateNumRequestsLabels(); 460 } 461 } 462 463 @Override 464 public void changedUpdate(DocumentEvent e) { 465 validateChunkSize(); 466 } 467 468 @Override 469 public void insertUpdate(DocumentEvent e) { 470 validateChunkSize(); 471 } 472 473 @Override 474 public void removeUpdate(DocumentEvent e) { 475 validateChunkSize(); 476 } 477 478 @Override 479 public void propertyChange(PropertyChangeEvent evt) { 480 if (evt.getSource() == tfChunkSize 481 && "enabled".equals(evt.getPropertyName()) 482 && (Boolean) evt.getNewValue() 483 ) { 484 validateChunkSize(); 485 } 486 } 487 } 488 489 class StrategyChangeListener extends FocusAdapter implements ItemListener, ActionListener { 490 491 protected void notifyStrategy() { 492 firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification()); 493 } 494 495 @Override 496 public void itemStateChanged(ItemEvent e) { 497 UploadStrategy strategy = getUploadStrategy(); 498 if (strategy == null) 499 return; 500 switch(strategy) { 501 case CHUNKED_DATASET_STRATEGY: 502 tfChunkSize.setEnabled(true); 503 tfChunkSize.requestFocusInWindow(); 504 break; 505 default: 506 tfChunkSize.setEnabled(false); 507 } 508 notifyStrategy(); 509 } 510 511 @Override 512 public void focusLost(FocusEvent e) { 513 notifyStrategy(); 514 } 515 516 @Override 517 public void actionPerformed(ActionEvent e) { 518 notifyStrategy(); 519 } 520 } 521}