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.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.util.EnumMap; 018import java.util.Map; 019import java.util.Map.Entry; 020 021import javax.swing.BorderFactory; 022import javax.swing.ButtonGroup; 023import javax.swing.JLabel; 024import javax.swing.JPanel; 025import javax.swing.JRadioButton; 026import javax.swing.text.JTextComponent; 027 028import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 029import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 030import org.openstreetmap.josm.gui.widgets.JosmTextField; 031import org.openstreetmap.josm.io.Capabilities; 032import org.openstreetmap.josm.io.MaxChangesetSizeExceededPolicy; 033import org.openstreetmap.josm.io.OsmApi; 034import org.openstreetmap.josm.io.UploadStrategy; 035import org.openstreetmap.josm.io.UploadStrategySpecification; 036import org.openstreetmap.josm.spi.preferences.Config; 037import org.openstreetmap.josm.tools.Logging; 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 { 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 transient Map<UploadStrategy, JRadioButton> rbStrategy; 054 private transient Map<UploadStrategy, JLabel> lblNumRequests; 055 private final JosmTextField tfChunkSize = new JosmTextField(4); 056 private final JPanel pnlMultiChangesetPolicyPanel = new JPanel(new GridBagLayout()); 057 private final JRadioButton rbFillOneChangeset = new JRadioButton(); 058 private final JRadioButton rbUseMultipleChangesets = new JRadioButton(); 059 private JMultilineLabel lblMultiChangesetPoliciesHeader; 060 061 private long numUploadedObjects; 062 063 /** 064 * Constructs a new {@code UploadStrategySelectionPanel}. 065 */ 066 public UploadStrategySelectionPanel() { 067 build(); 068 } 069 070 protected JPanel buildUploadStrategyPanel() { 071 JPanel pnl = new JPanel(new GridBagLayout()); 072 pnl.setBorder(BorderFactory.createTitledBorder(tr("Please select the upload strategy:"))); 073 ButtonGroup bgStrategies = new ButtonGroup(); 074 rbStrategy = new EnumMap<>(UploadStrategy.class); 075 lblNumRequests = new EnumMap<>(UploadStrategy.class); 076 for (UploadStrategy strategy: UploadStrategy.values()) { 077 rbStrategy.put(strategy, new JRadioButton()); 078 lblNumRequests.put(strategy, new JLabel()); 079 bgStrategies.add(rbStrategy.get(strategy)); 080 } 081 082 // -- single request strategy 083 GridBagConstraints gc = new GridBagConstraints(); 084 gc.gridx = 0; 085 gc.gridy = 1; 086 gc.weightx = 0.0; 087 gc.weighty = 0.0; 088 gc.gridwidth = 1; 089 gc.fill = GridBagConstraints.HORIZONTAL; 090 gc.insets = new Insets(3, 3, 3, 3); 091 gc.anchor = GridBagConstraints.FIRST_LINE_START; 092 JRadioButton radioButton = rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 093 radioButton.setText(tr("Upload all objects in one request")); 094 pnl.add(radioButton, gc); 095 gc.gridx = 2; 096 gc.weightx = 1.0; 097 pnl.add(lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY), gc); 098 099 // -- chunked dataset strategy 100 gc.gridy++; 101 gc.gridx = 0; 102 gc.weightx = 0.0; 103 radioButton = rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY); 104 radioButton.setText(tr("Upload objects in chunks of size: ")); 105 pnl.add(radioButton, gc); 106 gc.gridx = 1; 107 pnl.add(tfChunkSize, gc); 108 gc.gridx = 2; 109 pnl.add(lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY), gc); 110 111 // -- single request strategy 112 gc.gridy++; 113 gc.gridx = 0; 114 radioButton = rbStrategy.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY); 115 radioButton.setText(tr("Upload each object individually")); 116 pnl.add(radioButton, gc); 117 gc.gridx = 2; 118 pnl.add(lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY), gc); 119 120 new ChunkSizeValidator(tfChunkSize); 121 122 StrategyChangeListener strategyChangeListener = new StrategyChangeListener(); 123 tfChunkSize.addFocusListener(strategyChangeListener); 124 tfChunkSize.addActionListener(strategyChangeListener); 125 for (UploadStrategy strategy: UploadStrategy.values()) { 126 rbStrategy.get(strategy).addItemListener(strategyChangeListener); 127 } 128 129 return pnl; 130 } 131 132 protected JPanel buildMultiChangesetPolicyPanel() { 133 GridBagConstraints gc = new GridBagConstraints(); 134 gc.gridx = 0; 135 gc.gridy = 0; 136 gc.fill = GridBagConstraints.HORIZONTAL; 137 gc.anchor = GridBagConstraints.FIRST_LINE_START; 138 gc.insets = new Insets(3, 3, 3, 3); 139 gc.weightx = 1.0; 140 lblMultiChangesetPoliciesHeader = new JMultilineLabel( 141 tr("<html><strong>Multiple changesets</strong> are necessary to upload {0} objects. " + 142 "Please select a strategy:</html>", 143 numUploadedObjects)); 144 pnlMultiChangesetPolicyPanel.add(lblMultiChangesetPoliciesHeader, gc); 145 gc.gridy++; 146 rbFillOneChangeset.setText(tr("Fill up one changeset and return to the Upload Dialog")); 147 pnlMultiChangesetPolicyPanel.add(rbFillOneChangeset, gc); 148 gc.gridy++; 149 rbUseMultipleChangesets.setText(tr("Open and use as many new changesets as necessary")); 150 pnlMultiChangesetPolicyPanel.add(rbUseMultipleChangesets, gc); 151 152 ButtonGroup bgMultiChangesetPolicies = new ButtonGroup(); 153 bgMultiChangesetPolicies.add(rbFillOneChangeset); 154 bgMultiChangesetPolicies.add(rbUseMultipleChangesets); 155 return pnlMultiChangesetPolicyPanel; 156 } 157 158 protected void build() { 159 setLayout(new GridBagLayout()); 160 GridBagConstraints gc = new GridBagConstraints(); 161 gc.gridx = 0; 162 gc.gridy = 0; 163 gc.fill = GridBagConstraints.HORIZONTAL; 164 gc.weightx = 1.0; 165 gc.weighty = 0.0; 166 gc.anchor = GridBagConstraints.NORTHWEST; 167 168 add(buildUploadStrategyPanel(), gc); 169 gc.gridy = 1; 170 add(buildMultiChangesetPolicyPanel(), gc); 171 172 Capabilities capabilities = OsmApi.getOsmApi().getCapabilities(); 173 int maxChunkSize = capabilities != null ? capabilities.getMaxChangesetSize() : -1; 174 pnlMultiChangesetPolicyPanel.setVisible( 175 maxChunkSize > 0 && numUploadedObjects > maxChunkSize 176 ); 177 } 178 179 /** 180 * Sets the number of uploaded objects to display 181 * @param numUploadedObjects The number of objects 182 */ 183 public void setNumUploadedObjects(int numUploadedObjects) { 184 this.numUploadedObjects = Math.max(numUploadedObjects, 0); 185 updateNumRequestsLabels(); 186 } 187 188 /** 189 * Fills the inputs using a {@link UploadStrategySpecification} 190 * @param strategy The strategy 191 */ 192 public void setUploadStrategySpecification(UploadStrategySpecification strategy) { 193 if (strategy == null) 194 return; 195 rbStrategy.get(strategy.getStrategy()).setSelected(true); 196 tfChunkSize.setEnabled(strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY); 197 if (strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY) { 198 if (strategy.getChunkSize() != UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 199 tfChunkSize.setText(Integer.toString(strategy.getChunkSize())); 200 } else { 201 tfChunkSize.setText("1"); 202 } 203 } 204 } 205 206 /** 207 * Gets the upload strategy the user chose 208 * @return The strategy 209 */ 210 public UploadStrategySpecification getUploadStrategySpecification() { 211 UploadStrategy strategy = getUploadStrategy(); 212 UploadStrategySpecification spec = new UploadStrategySpecification(); 213 if (strategy != null) { 214 switch(strategy) { 215 case CHUNKED_DATASET_STRATEGY: 216 spec.setStrategy(strategy).setChunkSize(getChunkSize()); 217 break; 218 case INDIVIDUAL_OBJECTS_STRATEGY: 219 case SINGLE_REQUEST_STRATEGY: 220 default: 221 spec.setStrategy(strategy); 222 break; 223 } 224 } 225 if (pnlMultiChangesetPolicyPanel.isVisible()) { 226 if (rbFillOneChangeset.isSelected()) { 227 spec.setPolicy(MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG); 228 } else if (rbUseMultipleChangesets.isSelected()) { 229 spec.setPolicy(MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS); 230 } else { 231 spec.setPolicy(null); // unknown policy 232 } 233 } else { 234 spec.setPolicy(null); 235 } 236 return spec; 237 } 238 239 protected UploadStrategy getUploadStrategy() { 240 return rbStrategy.entrySet().stream() 241 .filter(e -> e.getValue().isSelected()) 242 .findFirst() 243 .map(Entry::getKey) 244 .orElse(null); 245 } 246 247 protected int getChunkSize() { 248 try { 249 return Integer.parseInt(tfChunkSize.getText().trim()); 250 } catch (NumberFormatException e) { 251 return UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE; 252 } 253 } 254 255 /** 256 * Load the panel contents from preferences 257 */ 258 public void initFromPreferences() { 259 UploadStrategy strategy = UploadStrategy.getFromPreferences(); 260 rbStrategy.get(strategy).setSelected(true); 261 int chunkSize = Config.getPref().getInt("osm-server.upload-strategy.chunk-size", 1000); 262 tfChunkSize.setText(Integer.toString(chunkSize)); 263 updateNumRequestsLabels(); 264 } 265 266 /** 267 * Stores the values that the user has input into the preferences 268 */ 269 public void rememberUserInput() { 270 UploadStrategy strategy = getUploadStrategy(); 271 UploadStrategy.saveToPreferences(strategy); 272 int chunkSize; 273 try { 274 chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 275 Config.getPref().putInt("osm-server.upload-strategy.chunk-size", chunkSize); 276 } catch (NumberFormatException e) { 277 // don't save invalid value to preferences 278 Logging.trace(e); 279 } 280 } 281 282 protected void updateNumRequestsLabels() { 283 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 284 if (maxChunkSize > 0 && numUploadedObjects > maxChunkSize) { 285 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(false); 286 JRadioButton lbl = rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 287 lbl.setEnabled(false); 288 lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>" 289 + "max. changeset size {1} on server ''{2}'' is exceeded.</html>", 290 numUploadedObjects, maxChunkSize, OsmApi.getOsmApi().getBaseUrl() 291 ) 292 ); 293 rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true); 294 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false); 295 296 lblMultiChangesetPoliciesHeader.setText( 297 tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " + 298 "Which strategy do you want to use?</html>", 299 numUploadedObjects)); 300 if (!rbFillOneChangeset.isSelected() && !rbUseMultipleChangesets.isSelected()) { 301 rbUseMultipleChangesets.setSelected(true); 302 } 303 pnlMultiChangesetPolicyPanel.setVisible(true); 304 305 } else { 306 rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true); 307 JRadioButton lbl = rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY); 308 lbl.setEnabled(true); 309 lbl.setToolTipText(null); 310 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true); 311 312 pnlMultiChangesetPolicyPanel.setVisible(false); 313 } 314 315 lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)")); 316 if (numUploadedObjects == 0) { 317 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)")); 318 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 319 } else { 320 lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText( 321 trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects) 322 ); 323 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 324 int chunkSize = getChunkSize(); 325 if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 326 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)")); 327 } else { 328 int chunks = (int) Math.ceil((double) numUploadedObjects / (double) chunkSize); 329 lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText( 330 trn("({0} request)", "({0} requests)", chunks, chunks) 331 ); 332 } 333 } 334 } 335 336 /** 337 * Sets the focus on the chunk size field 338 */ 339 public void initEditingOfChunkSize() { 340 tfChunkSize.requestFocusInWindow(); 341 } 342 343 class ChunkSizeValidator extends AbstractTextComponentValidator { 344 ChunkSizeValidator(JTextComponent tc) { 345 super(tc); 346 } 347 348 @Override 349 public void validate() { 350 try { 351 int chunkSize = Integer.parseInt(tfChunkSize.getText().trim()); 352 int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(); 353 if (chunkSize <= 0) { 354 feedbackInvalid(tr("Illegal chunk size <= 0. Please enter an integer > 1")); 355 } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 356 feedbackInvalid(tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", 357 chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 358 } else { 359 feedbackValid(null); 360 } 361 362 if (maxChunkSize > 0 && chunkSize > maxChunkSize) { 363 feedbackInvalid(tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", 364 chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl())); 365 } 366 } catch (NumberFormatException e) { 367 feedbackInvalid(tr("Value ''{0}'' is not a number. Please enter an integer > 1", 368 tfChunkSize.getText().trim())); 369 } finally { 370 updateNumRequestsLabels(); 371 } 372 } 373 374 @Override 375 public boolean isValid() { 376 throw new UnsupportedOperationException(); 377 } 378 } 379 380 class StrategyChangeListener implements FocusListener, ItemListener, ActionListener { 381 382 protected void notifyStrategy() { 383 firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification()); 384 } 385 386 @Override 387 public void itemStateChanged(ItemEvent e) { 388 UploadStrategy strategy = getUploadStrategy(); 389 if (strategy == null) 390 return; 391 switch(strategy) { 392 case CHUNKED_DATASET_STRATEGY: 393 tfChunkSize.setEnabled(true); 394 tfChunkSize.requestFocusInWindow(); 395 break; 396 default: 397 tfChunkSize.setEnabled(false); 398 } 399 notifyStrategy(); 400 } 401 402 @Override 403 public void focusGained(FocusEvent e) { 404 Component c = e.getComponent(); 405 if (c instanceof JosmTextField) { 406 JosmTextField tf = (JosmTextField) c; 407 tf.selectAll(); 408 } 409 } 410 411 @Override 412 public void focusLost(FocusEvent e) { 413 notifyStrategy(); 414 } 415 416 @Override 417 public void actionPerformed(ActionEvent e) { 418 notifyStrategy(); 419 } 420 } 421}