001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003import static org.openstreetmap.josm.tools.I18n.tr; 004import static org.openstreetmap.josm.tools.I18n.trn; 005 006import java.awt.Color; 007import java.awt.Component; 008import java.awt.GridBagConstraints; 009import java.awt.GridBagLayout; 010import java.awt.Insets; 011import java.awt.event.ActionEvent; 012import java.awt.event.ActionListener; 013import java.awt.event.FocusEvent; 014import java.awt.event.FocusListener; 015import java.awt.event.ItemEvent; 016import java.awt.event.ItemListener; 017import java.beans.PropertyChangeEvent; 018import java.beans.PropertyChangeListener; 019import java.util.HashMap; 020import java.util.Map; 021 022import javax.swing.BorderFactory; 023import javax.swing.ButtonGroup; 024import javax.swing.JLabel; 025import javax.swing.JPanel; 026import javax.swing.JRadioButton; 027import javax.swing.UIManager; 028import javax.swing.event.DocumentEvent; 029import javax.swing.event.DocumentListener; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.io.OsmApi; 033import org.openstreetmap.josm.tools.ImageProvider; 034import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 035import org.openstreetmap.josm.gui.widgets.JosmTextField; 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 final static 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<UploadStrategy, JRadioButton>(); 076 lblStrategies = new HashMap<UploadStrategy, JMultilineLabel>(); 077 lblNumRequests = new HashMap<UploadStrategy, JLabel>(); 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 = 0.0; 108 gc.weighty = 0.0; 109 gc.gridwidth = 2; 110 JLabel 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 = 1.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 = 0.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 = 1.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 = 0.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 = 1.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 JLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 323 lbl.setIcon(ImageProvider.get("warning-small.png")); 324 lbl.setText(tr("Upload in one request not possible (too many objects to upload)")); 325 lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>" 326 + "max. changeset size {1} on server ''{2}'' is exceeded.</html>", 327 numUploadedObjects, 328 maxChunkSize, 329 OsmApi.getOsmApi().getBaseUrl() 330 ) 331 ); 332 rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true); 333 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false); 334 335 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)); 336 if (!rbFillOneChangeset.isSelected() && ! rbUseMultipleChangesets.isSelected()) { 337 rbUseMultipleChangesets.setSelected(true); 338 } 339 pnlMultiChangesetPolicyPanel.setVisible(true); 340 341 } else { 342 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true); 343 JLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 344 lbl.setText(tr("Upload data in one request")); 345 lbl.setIcon(null); 346 lbl.setToolTipText(""); 347 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true); 348 349 pnlMultiChangesetPolicyPanel.setVisible(false); 350 } 351 352 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)")); 353 if (numUploadedObjects == 0) { 354 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)")); 355 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 356 } else { 357 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText( 358 trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects) 359 ); 360 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 361 int chunkSize = getChunkSize(); 362 if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 363 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 364 } else { 365 int chunks = (int)Math.ceil((double)numUploadedObjects / (double)chunkSize); 366 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText( 367 trn("({0} request)", "({0} requests)", chunks, chunks) 368 ); 369 } 370 } 371 } 372 373 public void initEditingOfChunkSize() { 374 tfChunkSize.requestFocusInWindow(); 375 } 376 377 @Override 378 public void propertyChange(PropertyChangeEvent evt) { 379 if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) { 380 setNumUploadedObjects((Integer)evt.getNewValue()); 381 } 382 } 383 384 static class TextFieldFocusHandler implements FocusListener { 385 @Override 386 public void focusGained(FocusEvent e) { 387 Component c = e.getComponent(); 388 if (c instanceof JosmTextField) { 389 JosmTextField tf = (JosmTextField)c; 390 tf.selectAll(); 391 } 392 } 393 @Override 394 public void focusLost(FocusEvent e) {} 395 } 396 397 class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener { 398 protected void setErrorFeedback(JosmTextField tf, String message) { 399 tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); 400 tf.setToolTipText(message); 401 tf.setBackground(BG_COLOR_ERROR); 402 } 403 404 protected void clearErrorFeedback(JosmTextField tf, String message) { 405 tf.setBorder(UIManager.getBorder("TextField.border")); 406 tf.setToolTipText(message); 407 tf.setBackground(UIManager.getColor("TextField.background")); 408 } 409 410 protected void valiateChunkSize() { 411 try { 412 int chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 413 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 414 if (chunkSize <= 0) { 415 setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1")); 416 } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 417 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 418 } else { 419 clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1")); 420 } 421 422 if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 423 setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 424 } 425 } catch(NumberFormatException e) { 426 setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1", tfChunkSize.getText().trim())); 427 } finally { 428 updateNumRequestsLabels(); 429 } 430 } 431 432 @Override 433 public void changedUpdate(DocumentEvent arg0) { 434 valiateChunkSize(); 435 } 436 437 @Override 438 public void insertUpdate(DocumentEvent arg0) { 439 valiateChunkSize(); 440 } 441 442 @Override 443 public void removeUpdate(DocumentEvent arg0) { 444 valiateChunkSize(); 445 } 446 447 @Override 448 public void propertyChange(PropertyChangeEvent evt) { 449 if (evt.getSource() == tfChunkSize 450 && evt.getPropertyName().equals("enabled") 451 && (Boolean)evt.getNewValue() 452 ) { 453 valiateChunkSize(); 454 } 455 } 456 } 457 458 class StrategyChangeListener implements ItemListener, FocusListener, ActionListener { 459 460 protected void notifyStrategy() { 461 firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification()); 462 } 463 464 @Override 465 public void itemStateChanged(ItemEvent e) { 466 UploadStrategy strategy = getUploadStrategy(); 467 if (strategy == null) return; 468 switch(strategy) { 469 case CHUNKED_DATASET_STRATEGY: 470 tfChunkSize.setEnabled(true); 471 tfChunkSize.requestFocusInWindow(); 472 break; 473 default: 474 tfChunkSize.setEnabled(false); 475 } 476 notifyStrategy(); 477 } 478 479 @Override 480 public void focusGained(FocusEvent arg0) {} 481 482 @Override 483 public void focusLost(FocusEvent arg0) { 484 notifyStrategy(); 485 } 486 487 @Override 488 public void actionPerformed(ActionEvent arg0) { 489 notifyStrategy(); 490 } 491 } 492}