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