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