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}