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.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 JosmTextField tfChunkSize; 059 private JPanel pnlMultiChangesetPolicyPanel; 060 private JRadioButton rbFillOneChangeset; 061 private JRadioButton rbUseMultipleChangesets; 062 private JMultilineLabel lblMultiChangesetPoliciesHeader; 063 064 private long numUploadedObjects; 065 066 /** 067 * Constructs a new {@code UploadStrategySelectionPanel}. 068 */ 069 public UploadStrategySelectionPanel() { 070 build(); 071 } 072 073 protected JPanel buildUploadStrategyPanel() { 074 JPanel pnl = new JPanel(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(new GridBagLayout()); 184 GridBagConstraints gc = new GridBagConstraints(); 185 gc.gridx = 0; 186 gc.gridy = 0; 187 gc.fill = GridBagConstraints.HORIZONTAL; 188 gc.anchor = GridBagConstraints.FIRST_LINE_START; 189 gc.weightx = 1.0; 190 pnlMultiChangesetPolicyPanel.add(lblMultiChangesetPoliciesHeader = new JMultilineLabel( 191 tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " + 192 "Which strategy do you want to use?</html>", 193 numUploadedObjects)), gc); 194 gc.gridy = 1; 195 pnlMultiChangesetPolicyPanel.add(rbFillOneChangeset = new JRadioButton( 196 tr("Fill up one changeset and return to the Upload Dialog")), gc); 197 gc.gridy = 2; 198 pnlMultiChangesetPolicyPanel.add(rbUseMultipleChangesets = new JRadioButton( 199 tr("Open and use as many new changesets as necessary")), 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 int chunkSize = getChunkSize(); 258 UploadStrategySpecification spec = new UploadStrategySpecification(); 259 if (strategy != null) { 260 switch(strategy) { 261 case INDIVIDUAL_OBJECTS_STRATEGY: 262 case SINGLE_REQUEST_STRATEGY: 263 spec.setStrategy(strategy); 264 break; 265 case CHUNKED_DATASET_STRATEGY: 266 spec.setStrategy(strategy).setChunkSize(chunkSize); 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 if (Main.isTraceEnabled()) { 321 Main.trace(e.getMessage()); 322 } 323 } 324 } 325 326 protected void updateNumRequestsLabels() { 327 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 328 if (maxChunkSize > 0 && numUploadedObjects > maxChunkSize) { 329 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(false); 330 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 331 lbl.setText(tr("Upload in one request not possible (too many objects to upload)")); 332 lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>" 333 + "max. changeset size {1} on server ''{2}'' is exceeded.</html>", 334 numUploadedObjects, maxChunkSize, OsmApi.getOsmApi().getBaseUrl() 335 ) 336 ); 337 rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true); 338 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false); 339 340 lblMultiChangesetPoliciesHeader.setText( 341 tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " + 342 "Which strategy do you want to use?</html>", 343 numUploadedObjects)); 344 if (!rbFillOneChangeset.isSelected() && !rbUseMultipleChangesets.isSelected()) { 345 rbUseMultipleChangesets.setSelected(true); 346 } 347 pnlMultiChangesetPolicyPanel.setVisible(true); 348 349 } else { 350 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true); 351 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 352 lbl.setText(tr("Upload data in one request")); 353 lbl.setToolTipText(null); 354 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true); 355 356 pnlMultiChangesetPolicyPanel.setVisible(false); 357 } 358 359 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)")); 360 if (numUploadedObjects == 0) { 361 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)")); 362 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 363 } else { 364 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText( 365 trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects) 366 ); 367 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 368 int chunkSize = getChunkSize(); 369 if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 370 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 371 } else { 372 int chunks = (int) Math.ceil((double) numUploadedObjects / (double) chunkSize); 373 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText( 374 trn("({0} request)", "({0} requests)", chunks, chunks) 375 ); 376 } 377 } 378 } 379 380 public void initEditingOfChunkSize() { 381 tfChunkSize.requestFocusInWindow(); 382 } 383 384 @Override 385 public void propertyChange(PropertyChangeEvent evt) { 386 if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) { 387 setNumUploadedObjects((Integer) evt.getNewValue()); 388 } 389 } 390 391 static class TextFieldFocusHandler implements FocusListener { 392 @Override 393 public void focusGained(FocusEvent e) { 394 Component c = e.getComponent(); 395 if (c instanceof JosmTextField) { 396 JosmTextField tf = (JosmTextField) c; 397 tf.selectAll(); 398 } 399 } 400 401 @Override 402 public void focusLost(FocusEvent e) {} 403 } 404 405 class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener { 406 protected void setErrorFeedback(JosmTextField tf, String message) { 407 tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); 408 tf.setToolTipText(message); 409 tf.setBackground(BG_COLOR_ERROR); 410 } 411 412 protected void clearErrorFeedback(JosmTextField tf, String message) { 413 tf.setBorder(UIManager.getBorder("TextField.border")); 414 tf.setToolTipText(message); 415 tf.setBackground(UIManager.getColor("TextField.background")); 416 } 417 418 protected void validateChunkSize() { 419 try { 420 int chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 421 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 422 if (chunkSize <= 0) { 423 setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1")); 424 } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 425 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", 426 chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 427 } else { 428 clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1")); 429 } 430 431 if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 432 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", 433 chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 434 } 435 } catch (NumberFormatException e) { 436 setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1", 437 tfChunkSize.getText().trim())); 438 } finally { 439 updateNumRequestsLabels(); 440 } 441 } 442 443 @Override 444 public void changedUpdate(DocumentEvent arg0) { 445 validateChunkSize(); 446 } 447 448 @Override 449 public void insertUpdate(DocumentEvent arg0) { 450 validateChunkSize(); 451 } 452 453 @Override 454 public void removeUpdate(DocumentEvent arg0) { 455 validateChunkSize(); 456 } 457 458 @Override 459 public void propertyChange(PropertyChangeEvent evt) { 460 if (evt.getSource() == tfChunkSize 461 && "enabled".equals(evt.getPropertyName()) 462 && (Boolean) evt.getNewValue() 463 ) { 464 validateChunkSize(); 465 } 466 } 467 } 468 469 class StrategyChangeListener implements ItemListener, FocusListener, ActionListener { 470 471 protected void notifyStrategy() { 472 firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification()); 473 } 474 475 @Override 476 public void itemStateChanged(ItemEvent e) { 477 UploadStrategy strategy = getUploadStrategy(); 478 if (strategy == null) return; 479 switch(strategy) { 480 case CHUNKED_DATASET_STRATEGY: 481 tfChunkSize.setEnabled(true); 482 tfChunkSize.requestFocusInWindow(); 483 break; 484 default: 485 tfChunkSize.setEnabled(false); 486 } 487 notifyStrategy(); 488 } 489 490 @Override 491 public void focusGained(FocusEvent arg0) {} 492 493 @Override 494 public void focusLost(FocusEvent arg0) { 495 notifyStrategy(); 496 } 497 498 @Override 499 public void actionPerformed(ActionEvent arg0) { 500 notifyStrategy(); 501 } 502 } 503}