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 spec.setStrategy(strategy); 255 break; 256 case SINGLE_REQUEST_STRATEGY: 257 spec.setStrategy(strategy); 258 break; 259 case CHUNKED_DATASET_STRATEGY: 260 spec.setStrategy(strategy).setChunkSize(chunkSize); 261 break; 262 } 263 if(pnlMultiChangesetPolicyPanel.isVisible()) { 264 if (rbFillOneChangeset.isSelected()) { 265 spec.setPolicy(MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG); 266 } else if (rbUseMultipleChangesets.isSelected()) { 267 spec.setPolicy(MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS); 268 } else { 269 spec.setPolicy(null); // unknown policy 270 } 271 } else { 272 spec.setPolicy(null); 273 } 274 return spec; 275 } 276 277 protected UploadStrategy getUploadStrategy() { 278 UploadStrategy strategy = null; 279 for (UploadStrategy s: rbStrategy.keySet()) { 280 if (rbStrategy.get(s).isSelected()) { 281 strategy = s; 282 break; 283 } 284 } 285 return strategy; 286 } 287 288 protected int getChunkSize() { 289 int chunkSize; 290 try { 291 chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 292 return chunkSize; 293 } catch(NumberFormatException e) { 294 return UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE; 295 } 296 } 297 298 public void initFromPreferences() { 299 UploadStrategy strategy = UploadStrategy.getFromPreferences(); 300 rbStrategy.get(strategy).setSelected(true); 301 int chunkSize = Main.pref.getInteger("osm-server.upload-strategy.chunk-size", 1); 302 tfChunkSize.setText(Integer.toString(chunkSize)); 303 updateNumRequestsLabels(); 304 } 305 306 public void rememberUserInput() { 307 UploadStrategy strategy = getUploadStrategy(); 308 UploadStrategy.saveToPreferences(strategy); 309 int chunkSize; 310 try { 311 chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 312 Main.pref.putInteger("osm-server.upload-strategy.chunk-size", chunkSize); 313 } catch(NumberFormatException e) { 314 // don't save invalid value to preferences 315 } 316 } 317 318 protected void updateNumRequestsLabels() { 319 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 320 if (maxChunkSize > 0 && numUploadedObjects > maxChunkSize) { 321 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(false); 322 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 323 lbl.setText(tr("Upload in one request not possible (too many objects to upload)")); 324 lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>" 325 + "max. changeset size {1} on server ''{2}'' is exceeded.</html>", 326 numUploadedObjects, 327 maxChunkSize, 328 OsmApi.getOsmApi().getBaseUrl() 329 ) 330 ); 331 rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true); 332 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false); 333 334 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)); 335 if (!rbFillOneChangeset.isSelected() && ! rbUseMultipleChangesets.isSelected()) { 336 rbUseMultipleChangesets.setSelected(true); 337 } 338 pnlMultiChangesetPolicyPanel.setVisible(true); 339 340 } else { 341 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true); 342 JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 343 lbl.setText(tr("Upload data in one request")); 344 lbl.setToolTipText(""); 345 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true); 346 347 pnlMultiChangesetPolicyPanel.setVisible(false); 348 } 349 350 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)")); 351 if (numUploadedObjects == 0) { 352 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)")); 353 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 354 } else { 355 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText( 356 trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects) 357 ); 358 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 359 int chunkSize = getChunkSize(); 360 if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 361 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 362 } else { 363 int chunks = (int)Math.ceil((double)numUploadedObjects / (double)chunkSize); 364 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText( 365 trn("({0} request)", "({0} requests)", chunks, chunks) 366 ); 367 } 368 } 369 } 370 371 public void initEditingOfChunkSize() { 372 tfChunkSize.requestFocusInWindow(); 373 } 374 375 @Override 376 public void propertyChange(PropertyChangeEvent evt) { 377 if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) { 378 setNumUploadedObjects((Integer)evt.getNewValue()); 379 } 380 } 381 382 static class TextFieldFocusHandler implements FocusListener { 383 @Override 384 public void focusGained(FocusEvent e) { 385 Component c = e.getComponent(); 386 if (c instanceof JosmTextField) { 387 JosmTextField tf = (JosmTextField)c; 388 tf.selectAll(); 389 } 390 } 391 @Override 392 public void focusLost(FocusEvent e) {} 393 } 394 395 class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener { 396 protected void setErrorFeedback(JosmTextField tf, String message) { 397 tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); 398 tf.setToolTipText(message); 399 tf.setBackground(BG_COLOR_ERROR); 400 } 401 402 protected void clearErrorFeedback(JosmTextField tf, String message) { 403 tf.setBorder(UIManager.getBorder("TextField.border")); 404 tf.setToolTipText(message); 405 tf.setBackground(UIManager.getColor("TextField.background")); 406 } 407 408 protected void validateChunkSize() { 409 try { 410 int chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 411 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 412 if (chunkSize <= 0) { 413 setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1")); 414 } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 415 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 416 } else { 417 clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1")); 418 } 419 420 if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 421 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 422 } 423 } catch(NumberFormatException e) { 424 setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1", tfChunkSize.getText().trim())); 425 } finally { 426 updateNumRequestsLabels(); 427 } 428 } 429 430 @Override 431 public void changedUpdate(DocumentEvent arg0) { 432 validateChunkSize(); 433 } 434 435 @Override 436 public void insertUpdate(DocumentEvent arg0) { 437 validateChunkSize(); 438 } 439 440 @Override 441 public void removeUpdate(DocumentEvent arg0) { 442 validateChunkSize(); 443 } 444 445 @Override 446 public void propertyChange(PropertyChangeEvent evt) { 447 if (evt.getSource() == tfChunkSize 448 && "enabled".equals(evt.getPropertyName()) 449 && (Boolean)evt.getNewValue() 450 ) { 451 validateChunkSize(); 452 } 453 } 454 } 455 456 class StrategyChangeListener implements ItemListener, FocusListener, ActionListener { 457 458 protected void notifyStrategy() { 459 firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification()); 460 } 461 462 @Override 463 public void itemStateChanged(ItemEvent e) { 464 UploadStrategy strategy = getUploadStrategy(); 465 if (strategy == null) return; 466 switch(strategy) { 467 case CHUNKED_DATASET_STRATEGY: 468 tfChunkSize.setEnabled(true); 469 tfChunkSize.requestFocusInWindow(); 470 break; 471 default: 472 tfChunkSize.setEnabled(false); 473 } 474 notifyStrategy(); 475 } 476 477 @Override 478 public void focusGained(FocusEvent arg0) {} 479 480 @Override 481 public void focusLost(FocusEvent arg0) { 482 notifyStrategy(); 483 } 484 485 @Override 486 public void actionPerformed(ActionEvent arg0) { 487 notifyStrategy(); 488 } 489 } 490}