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