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