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            if (Main.isTraceEnabled()) {
321                Main.trace(e.getMessage());
322            }
323        }
324    }
325
326    protected void updateNumRequestsLabels() {
327        int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize();
328        if (maxChunkSize > 0 && numUploadedObjects > maxChunkSize) {
329            rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(false);
330            JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
331            lbl.setText(tr("Upload in one request not possible (too many objects to upload)"));
332            lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>"
333                    + "max. changeset size {1} on server ''{2}'' is exceeded.</html>",
334                    numUploadedObjects, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()
335            )
336            );
337            rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true);
338            lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false);
339
340            lblMultiChangesetPoliciesHeader.setText(
341                    tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " +
342                       "Which strategy do you want to use?</html>",
343                            numUploadedObjects));
344            if (!rbFillOneChangeset.isSelected() && !rbUseMultipleChangesets.isSelected()) {
345                rbUseMultipleChangesets.setSelected(true);
346            }
347            pnlMultiChangesetPolicyPanel.setVisible(true);
348
349        } else {
350            rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true);
351            JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
352            lbl.setText(tr("Upload data in one request"));
353            lbl.setToolTipText(null);
354            lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true);
355
356            pnlMultiChangesetPolicyPanel.setVisible(false);
357        }
358
359        lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)"));
360        if (numUploadedObjects == 0) {
361            lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)"));
362            lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
363        } else {
364            lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(
365                    trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects)
366            );
367            lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
368            int chunkSize = getChunkSize();
369            if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
370                lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
371            } else {
372                int chunks = (int) Math.ceil((double) numUploadedObjects / (double) chunkSize);
373                lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(
374                        trn("({0} request)", "({0} requests)", chunks, chunks)
375                );
376            }
377        }
378    }
379
380    public void initEditingOfChunkSize() {
381        tfChunkSize.requestFocusInWindow();
382    }
383
384    @Override
385    public void propertyChange(PropertyChangeEvent evt) {
386        if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) {
387            setNumUploadedObjects((Integer) evt.getNewValue());
388        }
389    }
390
391    static class TextFieldFocusHandler extends FocusAdapter {
392        @Override
393        public void focusGained(FocusEvent e) {
394            Component c = e.getComponent();
395            if (c instanceof JosmTextField) {
396                JosmTextField tf = (JosmTextField) c;
397                tf.selectAll();
398            }
399        }
400    }
401
402    class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener {
403        protected void setErrorFeedback(JosmTextField tf, String message) {
404            tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
405            tf.setToolTipText(message);
406            tf.setBackground(BG_COLOR_ERROR);
407        }
408
409        protected void clearErrorFeedback(JosmTextField tf, String message) {
410            tf.setBorder(UIManager.getBorder("TextField.border"));
411            tf.setToolTipText(message);
412            tf.setBackground(UIManager.getColor("TextField.background"));
413        }
414
415        protected void validateChunkSize() {
416            try {
417                int chunkSize = Integer.parseInt(tfChunkSize.getText().trim());
418                int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize();
419                if (chunkSize <= 0) {
420                    setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1"));
421                } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) {
422                    setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''",
423                            chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()));
424                } else {
425                    clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1"));
426                }
427
428                if (maxChunkSize > 0 && chunkSize > maxChunkSize) {
429                    setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''",
430                            chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()));
431                }
432            } catch (NumberFormatException e) {
433                setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1",
434                        tfChunkSize.getText().trim()));
435            } finally {
436                updateNumRequestsLabels();
437            }
438        }
439
440        @Override
441        public void changedUpdate(DocumentEvent arg0) {
442            validateChunkSize();
443        }
444
445        @Override
446        public void insertUpdate(DocumentEvent arg0) {
447            validateChunkSize();
448        }
449
450        @Override
451        public void removeUpdate(DocumentEvent arg0) {
452            validateChunkSize();
453        }
454
455        @Override
456        public void propertyChange(PropertyChangeEvent evt) {
457            if (evt.getSource() == tfChunkSize
458                    && "enabled".equals(evt.getPropertyName())
459                    && (Boolean) evt.getNewValue()
460            ) {
461                validateChunkSize();
462            }
463        }
464    }
465
466    class StrategyChangeListener extends FocusAdapter implements ItemListener, ActionListener {
467
468        protected void notifyStrategy() {
469            firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification());
470        }
471
472        @Override
473        public void itemStateChanged(ItemEvent e) {
474            UploadStrategy strategy = getUploadStrategy();
475            if (strategy == null)
476                return;
477            switch(strategy) {
478            case CHUNKED_DATASET_STRATEGY:
479                tfChunkSize.setEnabled(true);
480                tfChunkSize.requestFocusInWindow();
481                break;
482            default:
483                tfChunkSize.setEnabled(false);
484            }
485            notifyStrategy();
486        }
487
488        @Override
489        public void focusLost(FocusEvent arg0) {
490            notifyStrategy();
491        }
492
493        @Override
494        public void actionPerformed(ActionEvent arg0) {
495            notifyStrategy();
496        }
497    }
498}