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.FocusEvent;
015import java.awt.event.FocusListener;
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 JosmTextField tfChunkSize;
059    private JPanel pnlMultiChangesetPolicyPanel;
060    private JRadioButton rbFillOneChangeset;
061    private JRadioButton rbUseMultipleChangesets;
062    private JMultilineLabel lblMultiChangesetPoliciesHeader;
063
064    private long numUploadedObjects;
065
066    /**
067     * Constructs a new {@code UploadStrategySelectionPanel}.
068     */
069    public UploadStrategySelectionPanel() {
070        build();
071    }
072
073    protected JPanel buildUploadStrategyPanel() {
074        JPanel pnl = new JPanel(new GridBagLayout());
075        ButtonGroup bgStrategies = new ButtonGroup();
076        rbStrategy = new EnumMap<>(UploadStrategy.class);
077        lblStrategies = new EnumMap<>(UploadStrategy.class);
078        lblNumRequests = new EnumMap<>(UploadStrategy.class);
079        for (UploadStrategy strategy: UploadStrategy.values()) {
080            rbStrategy.put(strategy, new JRadioButton());
081            lblNumRequests.put(strategy, new JLabel());
082            lblStrategies.put(strategy, new JMultilineLabel(""));
083            bgStrategies.add(rbStrategy.get(strategy));
084        }
085
086        // -- headline
087        GridBagConstraints gc = new GridBagConstraints();
088        gc.gridx = 0;
089        gc.gridy = 0;
090        gc.weightx = 1.0;
091        gc.weighty = 0.0;
092        gc.gridwidth = 4;
093        gc.fill = GridBagConstraints.HORIZONTAL;
094        gc.insets = new Insets(0, 0, 3, 0);
095        gc.anchor = GridBagConstraints.FIRST_LINE_START;
096        pnl.add(new JMultilineLabel(tr("Please select the upload strategy:")), gc);
097
098        // -- single request strategy
099        gc.gridx = 0;
100        gc.gridy = 1;
101        gc.weightx = 0.0;
102        gc.weighty = 0.0;
103        gc.gridwidth = 1;
104        gc.anchor = GridBagConstraints.FIRST_LINE_START;
105        pnl.add(rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY), gc);
106        gc.gridx = 1;
107        gc.gridy = 1;
108        gc.weightx = 1.0;
109        gc.weighty = 0.0;
110        gc.gridwidth = 2;
111        JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
112        lbl.setText(tr("Upload data in one request"));
113        pnl.add(lbl, gc);
114        gc.gridx = 3;
115        gc.gridy = 1;
116        gc.weightx = 0.0;
117        gc.weighty = 0.0;
118        gc.gridwidth = 1;
119        pnl.add(lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY), gc);
120
121        // -- chunked dataset strategy
122        gc.gridx = 0;
123        gc.gridy = 2;
124        gc.weightx = 0.0;
125        gc.weighty = 0.0;
126        pnl.add(rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY), gc);
127        gc.gridx = 1;
128        gc.gridy = 2;
129        gc.weightx = 1.0;
130        gc.weighty = 0.0;
131        gc.gridwidth = 1;
132        lbl = lblStrategies.get(UploadStrategy.CHUNKED_DATASET_STRATEGY);
133        lbl.setText(tr("Upload data in chunks of objects. Chunk size: "));
134        pnl.add(lbl, gc);
135        gc.gridx = 2;
136        gc.gridy = 2;
137        gc.weightx = 0.0;
138        gc.weighty = 0.0;
139        gc.gridwidth = 1;
140        pnl.add(tfChunkSize = new JosmTextField(4), gc);
141        gc.gridx = 3;
142        gc.gridy = 2;
143        gc.weightx = 0.0;
144        gc.weighty = 0.0;
145        gc.gridwidth = 1;
146        pnl.add(lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY), gc);
147
148        // -- single request strategy
149        gc.gridx = 0;
150        gc.gridy = 3;
151        gc.weightx = 0.0;
152        gc.weighty = 0.0;
153        pnl.add(rbStrategy.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY), gc);
154        gc.gridx = 1;
155        gc.gridy = 3;
156        gc.weightx = 1.0;
157        gc.weighty = 0.0;
158        gc.gridwidth = 2;
159        lbl = lblStrategies.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY);
160        lbl.setText(tr("Upload each object individually"));
161        pnl.add(lbl, gc);
162        gc.gridx = 3;
163        gc.gridy = 3;
164        gc.weightx = 0.0;
165        gc.weighty = 0.0;
166        gc.gridwidth = 1;
167        pnl.add(lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY), gc);
168
169        tfChunkSize.addFocusListener(new TextFieldFocusHandler());
170        tfChunkSize.getDocument().addDocumentListener(new ChunkSizeInputVerifier());
171
172        StrategyChangeListener strategyChangeListener = new StrategyChangeListener();
173        tfChunkSize.addFocusListener(strategyChangeListener);
174        tfChunkSize.addActionListener(strategyChangeListener);
175        for (UploadStrategy strategy: UploadStrategy.values()) {
176            rbStrategy.get(strategy).addItemListener(strategyChangeListener);
177        }
178
179        return pnl;
180    }
181
182    protected JPanel buildMultiChangesetPolicyPanel() {
183        pnlMultiChangesetPolicyPanel = new JPanel(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(
191                tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " +
192                   "Which strategy do you want to use?</html>",
193                        numUploadedObjects)), gc);
194        gc.gridy = 1;
195        pnlMultiChangesetPolicyPanel.add(rbFillOneChangeset = new JRadioButton(
196                tr("Fill up one changeset and return to the Upload Dialog")), gc);
197        gc.gridy = 2;
198        pnlMultiChangesetPolicyPanel.add(rbUseMultipleChangesets = new JRadioButton(
199                tr("Open and use as many new changesets as necessary")), 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        int chunkSize = getChunkSize();
258        UploadStrategySpecification spec = new UploadStrategySpecification();
259        if (strategy != null) {
260            switch(strategy) {
261            case INDIVIDUAL_OBJECTS_STRATEGY:
262            case SINGLE_REQUEST_STRATEGY:
263                spec.setStrategy(strategy);
264                break;
265            case CHUNKED_DATASET_STRATEGY:
266                spec.setStrategy(strategy).setChunkSize(chunkSize);
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 implements FocusListener {
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        @Override
402        public void focusLost(FocusEvent e) {}
403    }
404
405    class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener {
406        protected void setErrorFeedback(JosmTextField tf, String message) {
407            tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
408            tf.setToolTipText(message);
409            tf.setBackground(BG_COLOR_ERROR);
410        }
411
412        protected void clearErrorFeedback(JosmTextField tf, String message) {
413            tf.setBorder(UIManager.getBorder("TextField.border"));
414            tf.setToolTipText(message);
415            tf.setBackground(UIManager.getColor("TextField.background"));
416        }
417
418        protected void validateChunkSize() {
419            try {
420                int chunkSize = Integer.parseInt(tfChunkSize.getText().trim());
421                int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize();
422                if (chunkSize <= 0) {
423                    setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1"));
424                } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) {
425                    setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''",
426                            chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()));
427                } else {
428                    clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1"));
429                }
430
431                if (maxChunkSize > 0 && chunkSize > maxChunkSize) {
432                    setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''",
433                            chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()));
434                }
435            } catch (NumberFormatException e) {
436                setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1",
437                        tfChunkSize.getText().trim()));
438            } finally {
439                updateNumRequestsLabels();
440            }
441        }
442
443        @Override
444        public void changedUpdate(DocumentEvent arg0) {
445            validateChunkSize();
446        }
447
448        @Override
449        public void insertUpdate(DocumentEvent arg0) {
450            validateChunkSize();
451        }
452
453        @Override
454        public void removeUpdate(DocumentEvent arg0) {
455            validateChunkSize();
456        }
457
458        @Override
459        public void propertyChange(PropertyChangeEvent evt) {
460            if (evt.getSource() == tfChunkSize
461                    && "enabled".equals(evt.getPropertyName())
462                    && (Boolean) evt.getNewValue()
463            ) {
464                validateChunkSize();
465            }
466        }
467    }
468
469    class StrategyChangeListener implements ItemListener, FocusListener, ActionListener {
470
471        protected void notifyStrategy() {
472            firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification());
473        }
474
475        @Override
476        public void itemStateChanged(ItemEvent e) {
477            UploadStrategy strategy = getUploadStrategy();
478            if (strategy == null) return;
479            switch(strategy) {
480            case CHUNKED_DATASET_STRATEGY:
481                tfChunkSize.setEnabled(true);
482                tfChunkSize.requestFocusInWindow();
483                break;
484            default:
485                tfChunkSize.setEnabled(false);
486            }
487            notifyStrategy();
488        }
489
490        @Override
491        public void focusGained(FocusEvent arg0) {}
492
493        @Override
494        public void focusLost(FocusEvent arg0) {
495            notifyStrategy();
496        }
497
498        @Override
499        public void actionPerformed(ActionEvent arg0) {
500            notifyStrategy();
501        }
502    }
503}