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