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.OsmApi;
037
038/**
039 * UploadStrategySelectionPanel is a panel for selecting an upload strategy.
040 *
041 * Clients can listen for property change events for the property
042 * {@link #UPLOAD_STRATEGY_SPECIFICATION_PROP}.
043 */
044public class UploadStrategySelectionPanel extends JPanel implements PropertyChangeListener {
045
046    /**
047     * The property for the upload strategy
048     */
049    public static final String UPLOAD_STRATEGY_SPECIFICATION_PROP =
050        UploadStrategySelectionPanel.class.getName() + ".uploadStrategySpecification";
051
052    private static final Color BG_COLOR_ERROR = new Color(255, 224, 224);
053
054    private transient Map<UploadStrategy, JRadioButton> rbStrategy;
055    private transient Map<UploadStrategy, JLabel> lblNumRequests;
056    private transient Map<UploadStrategy, JMultilineLabel> lblStrategies;
057    private JosmTextField tfChunkSize;
058    private JPanel pnlMultiChangesetPolicyPanel;
059    private JRadioButton rbFillOneChangeset;
060    private JRadioButton rbUseMultipleChangesets;
061    private JMultilineLabel lblMultiChangesetPoliciesHeader;
062
063    private long numUploadedObjects;
064
065    /**
066     * Constructs a new {@code UploadStrategySelectionPanel}.
067     */
068    public UploadStrategySelectionPanel() {
069        build();
070    }
071
072    protected JPanel buildUploadStrategyPanel() {
073        JPanel pnl = new JPanel();
074        pnl.setLayout(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();
184        pnlMultiChangesetPolicyPanel.setLayout(new GridBagLayout());
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        pnlMultiChangesetPolicyPanel.add(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)), gc);
195        gc.gridy = 1;
196        pnlMultiChangesetPolicyPanel.add(rbFillOneChangeset = new JRadioButton(
197                tr("Fill up one changeset and return to the Upload Dialog")), gc);
198        gc.gridy = 2;
199        pnlMultiChangesetPolicyPanel.add(rbUseMultipleChangesets = new JRadioButton(
200                tr("Open and use as many new changesets as necessary")), gc);
201
202        ButtonGroup bgMultiChangesetPolicies = new ButtonGroup();
203        bgMultiChangesetPolicies.add(rbFillOneChangeset);
204        bgMultiChangesetPolicies.add(rbUseMultipleChangesets);
205        return pnlMultiChangesetPolicyPanel;
206    }
207
208    protected void build() {
209        setLayout(new GridBagLayout());
210        GridBagConstraints gc = new GridBagConstraints();
211        gc.gridx = 0;
212        gc.gridy = 0;
213        gc.fill = GridBagConstraints.HORIZONTAL;
214        gc.weightx = 1.0;
215        gc.weighty = 0.0;
216        gc.anchor = GridBagConstraints.NORTHWEST;
217        gc.insets = new Insets(3, 3, 3, 3);
218
219        add(buildUploadStrategyPanel(), gc);
220        gc.gridy = 1;
221        add(buildMultiChangesetPolicyPanel(), gc);
222
223        // consume remaining space
224        gc.gridy = 2;
225        gc.fill = GridBagConstraints.BOTH;
226        gc.weightx = 1.0;
227        gc.weighty = 1.0;
228        add(new JPanel(), gc);
229
230        int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize();
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) return;
243        rbStrategy.get(strategy.getStrategy()).setSelected(true);
244        tfChunkSize.setEnabled(strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY);
245        if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)) {
246            if (strategy.getChunkSize() != UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
247                tfChunkSize.setText(Integer.toString(strategy.getChunkSize()));
248            } else {
249                tfChunkSize.setText("1");
250            }
251        }
252    }
253
254    public UploadStrategySpecification getUploadStrategySpecification() {
255        UploadStrategy strategy = getUploadStrategy();
256        int chunkSize = getChunkSize();
257        UploadStrategySpecification spec = new UploadStrategySpecification();
258        switch(strategy) {
259        case INDIVIDUAL_OBJECTS_STRATEGY:
260        case SINGLE_REQUEST_STRATEGY:
261            spec.setStrategy(strategy);
262            break;
263        case CHUNKED_DATASET_STRATEGY:
264            spec.setStrategy(strategy).setChunkSize(chunkSize);
265            break;
266        }
267        if (pnlMultiChangesetPolicyPanel.isVisible()) {
268            if (rbFillOneChangeset.isSelected()) {
269                spec.setPolicy(MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG);
270            } else if (rbUseMultipleChangesets.isSelected()) {
271                spec.setPolicy(MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS);
272            } else {
273                spec.setPolicy(null); // unknown policy
274            }
275        } else {
276            spec.setPolicy(null);
277        }
278        return spec;
279    }
280
281    protected UploadStrategy getUploadStrategy() {
282        UploadStrategy strategy = null;
283        for (Entry<UploadStrategy, JRadioButton> e : rbStrategy.entrySet()) {
284            if (e.getValue().isSelected()) {
285                strategy = e.getKey();
286                break;
287            }
288        }
289        return strategy;
290    }
291
292    protected int getChunkSize() {
293        try {
294            return Integer.parseInt(tfChunkSize.getText().trim());
295        } catch (NumberFormatException e) {
296            return UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE;
297        }
298    }
299
300    public void initFromPreferences() {
301        UploadStrategy strategy = UploadStrategy.getFromPreferences();
302        rbStrategy.get(strategy).setSelected(true);
303        int chunkSize = Main.pref.getInteger("osm-server.upload-strategy.chunk-size", 1);
304        tfChunkSize.setText(Integer.toString(chunkSize));
305        updateNumRequestsLabels();
306    }
307
308    public void rememberUserInput() {
309        UploadStrategy strategy = getUploadStrategy();
310        UploadStrategy.saveToPreferences(strategy);
311        int chunkSize;
312        try {
313            chunkSize = Integer.parseInt(tfChunkSize.getText().trim());
314            Main.pref.putInteger("osm-server.upload-strategy.chunk-size", chunkSize);
315        } catch (NumberFormatException e) {
316            // don't save invalid value to preferences
317            if (Main.isTraceEnabled()) {
318                Main.trace(e.getMessage());
319            }
320        }
321    }
322
323    protected void updateNumRequestsLabels() {
324        int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize();
325        if (maxChunkSize > 0 && numUploadedObjects > maxChunkSize) {
326            rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(false);
327            JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
328            lbl.setText(tr("Upload in one request not possible (too many objects to upload)"));
329            lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>"
330                    + "max. changeset size {1} on server ''{2}'' is exceeded.</html>",
331                    numUploadedObjects, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()
332            )
333            );
334            rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true);
335            lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false);
336
337            lblMultiChangesetPoliciesHeader.setText(
338                    tr("<html>There are <strong>multiple changesets</strong> necessary in order to upload {0} objects. " +
339                       "Which strategy do you want to use?</html>",
340                            numUploadedObjects));
341            if (!rbFillOneChangeset.isSelected() && !rbUseMultipleChangesets.isSelected()) {
342                rbUseMultipleChangesets.setSelected(true);
343            }
344            pnlMultiChangesetPolicyPanel.setVisible(true);
345
346        } else {
347            rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true);
348            JMultilineLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
349            lbl.setText(tr("Upload data in one request"));
350            lbl.setToolTipText(null);
351            lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true);
352
353            pnlMultiChangesetPolicyPanel.setVisible(false);
354        }
355
356        lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)"));
357        if (numUploadedObjects == 0) {
358            lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)"));
359            lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
360        } else {
361            lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(
362                    trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects)
363            );
364            lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
365            int chunkSize = getChunkSize();
366            if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
367                lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
368            } else {
369                int chunks = (int) Math.ceil((double) numUploadedObjects / (double) chunkSize);
370                lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(
371                        trn("({0} request)", "({0} requests)", chunks, chunks)
372                );
373            }
374        }
375    }
376
377    public void initEditingOfChunkSize() {
378        tfChunkSize.requestFocusInWindow();
379    }
380
381    @Override
382    public void propertyChange(PropertyChangeEvent evt) {
383        if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) {
384            setNumUploadedObjects((Integer) evt.getNewValue());
385        }
386    }
387
388    static class TextFieldFocusHandler implements FocusListener {
389        @Override
390        public void focusGained(FocusEvent e) {
391            Component c = e.getComponent();
392            if (c instanceof JosmTextField) {
393                JosmTextField tf = (JosmTextField) c;
394                tf.selectAll();
395            }
396        }
397
398        @Override
399        public void focusLost(FocusEvent e) {}
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 implements ItemListener, FocusListener, 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) return;
476            switch(strategy) {
477            case CHUNKED_DATASET_STRATEGY:
478                tfChunkSize.setEnabled(true);
479                tfChunkSize.requestFocusInWindow();
480                break;
481            default:
482                tfChunkSize.setEnabled(false);
483            }
484            notifyStrategy();
485        }
486
487        @Override
488        public void focusGained(FocusEvent arg0) {}
489
490        @Override
491        public void focusLost(FocusEvent arg0) {
492            notifyStrategy();
493        }
494
495        @Override
496        public void actionPerformed(ActionEvent arg0) {
497            notifyStrategy();
498        }
499    }
500}