001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003import static org.openstreetmap.josm.tools.I18n.tr;
004import static org.openstreetmap.josm.tools.I18n.trn;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Insets;
011import java.awt.event.ActionEvent;
012import java.awt.event.ActionListener;
013import java.awt.event.FocusEvent;
014import java.awt.event.FocusListener;
015import java.awt.event.ItemEvent;
016import java.awt.event.ItemListener;
017import java.beans.PropertyChangeEvent;
018import java.beans.PropertyChangeListener;
019import java.util.HashMap;
020import java.util.Map;
021
022import javax.swing.BorderFactory;
023import javax.swing.ButtonGroup;
024import javax.swing.JLabel;
025import javax.swing.JPanel;
026import javax.swing.JRadioButton;
027import javax.swing.UIManager;
028import javax.swing.event.DocumentEvent;
029import javax.swing.event.DocumentListener;
030
031import org.openstreetmap.josm.Main;
032import org.openstreetmap.josm.io.OsmApi;
033import org.openstreetmap.josm.tools.ImageProvider;
034import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
035import org.openstreetmap.josm.gui.widgets.JosmTextField;
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 final static 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<UploadStrategy, JRadioButton>();
076        lblStrategies = new HashMap<UploadStrategy, JMultilineLabel>();
077        lblNumRequests = new HashMap<UploadStrategy, JLabel>();
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        JLabel 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            JLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
323            lbl.setIcon(ImageProvider.get("warning-small.png"));
324            lbl.setText(tr("Upload in one request not possible (too many objects to upload)"));
325            lbl.setToolTipText(tr("<html>Cannot upload {0} objects in one request because the<br>"
326                    + "max. changeset size {1} on server ''{2}'' is exceeded.</html>",
327                    numUploadedObjects,
328                    maxChunkSize,
329                    OsmApi.getOsmApi().getBaseUrl()
330            )
331            );
332            rbStrategy.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setSelected(true);
333            lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(false);
334
335            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));
336            if (!rbFillOneChangeset.isSelected() && ! rbUseMultipleChangesets.isSelected()) {
337                rbUseMultipleChangesets.setSelected(true);
338            }
339            pnlMultiChangesetPolicyPanel.setVisible(true);
340
341        } else {
342            rbStrategy.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setEnabled(true);
343            JLabel lbl = lblStrategies.get(UploadStrategy.SINGLE_REQUEST_STRATEGY);
344            lbl.setText(tr("Upload data in one request"));
345            lbl.setIcon(null);
346            lbl.setToolTipText("");
347            lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setVisible(true);
348
349            pnlMultiChangesetPolicyPanel.setVisible(false);
350        }
351
352        lblNumRequests.get(UploadStrategy.SINGLE_REQUEST_STRATEGY).setText(tr("(1 request)"));
353        if (numUploadedObjects == 0) {
354            lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(tr("(# requests unknown)"));
355            lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
356        } else {
357            lblNumRequests.get(UploadStrategy.INDIVIDUAL_OBJECTS_STRATEGY).setText(
358                    trn("({0} request)", "({0} requests)", numUploadedObjects, numUploadedObjects)
359            );
360            lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
361            int chunkSize = getChunkSize();
362            if (chunkSize == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
363                lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(tr("(# requests unknown)"));
364            } else {
365                int chunks = (int)Math.ceil((double)numUploadedObjects / (double)chunkSize);
366                lblNumRequests.get(UploadStrategy.CHUNKED_DATASET_STRATEGY).setText(
367                        trn("({0} request)", "({0} requests)", chunks, chunks)
368                );
369            }
370        }
371    }
372
373    public void initEditingOfChunkSize() {
374        tfChunkSize.requestFocusInWindow();
375    }
376
377    @Override
378    public void propertyChange(PropertyChangeEvent evt) {
379        if (evt.getPropertyName().equals(UploadedObjectsSummaryPanel.NUM_OBJECTS_TO_UPLOAD_PROP)) {
380            setNumUploadedObjects((Integer)evt.getNewValue());
381        }
382    }
383
384    static class TextFieldFocusHandler implements FocusListener {
385        @Override
386        public void focusGained(FocusEvent e) {
387            Component c = e.getComponent();
388            if (c instanceof JosmTextField) {
389                JosmTextField tf = (JosmTextField)c;
390                tf.selectAll();
391            }
392        }
393        @Override
394        public void focusLost(FocusEvent e) {}
395    }
396
397    class ChunkSizeInputVerifier implements DocumentListener, PropertyChangeListener {
398        protected void setErrorFeedback(JosmTextField tf, String message) {
399            tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
400            tf.setToolTipText(message);
401            tf.setBackground(BG_COLOR_ERROR);
402        }
403
404        protected void clearErrorFeedback(JosmTextField tf, String message) {
405            tf.setBorder(UIManager.getBorder("TextField.border"));
406            tf.setToolTipText(message);
407            tf.setBackground(UIManager.getColor("TextField.background"));
408        }
409
410        protected void valiateChunkSize() {
411            try {
412                int chunkSize = Integer.parseInt(tfChunkSize.getText().trim());
413                int maxChunkSize = OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize();
414                if (chunkSize <= 0) {
415                    setErrorFeedback(tfChunkSize, tr("Illegal chunk size <= 0. Please enter an integer > 1"));
416                } else if (maxChunkSize > 0 && chunkSize > maxChunkSize) {
417                    setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()));
418                } else {
419                    clearErrorFeedback(tfChunkSize, tr("Please enter an integer > 1"));
420                }
421
422                if (maxChunkSize > 0 && chunkSize > maxChunkSize) {
423                    setErrorFeedback(tfChunkSize, tr("Chunk size {0} exceeds max. changeset size {1} for server ''{2}''", chunkSize, maxChunkSize, OsmApi.getOsmApi().getBaseUrl()));
424                }
425            } catch(NumberFormatException e) {
426                setErrorFeedback(tfChunkSize, tr("Value ''{0}'' is not a number. Please enter an integer > 1", tfChunkSize.getText().trim()));
427            } finally {
428                updateNumRequestsLabels();
429            }
430        }
431
432        @Override
433        public void changedUpdate(DocumentEvent arg0) {
434            valiateChunkSize();
435        }
436
437        @Override
438        public void insertUpdate(DocumentEvent arg0) {
439            valiateChunkSize();
440        }
441
442        @Override
443        public void removeUpdate(DocumentEvent arg0) {
444            valiateChunkSize();
445        }
446
447        @Override
448        public void propertyChange(PropertyChangeEvent evt) {
449            if (evt.getSource() == tfChunkSize
450                    && evt.getPropertyName().equals("enabled")
451                    && (Boolean)evt.getNewValue()
452            ) {
453                valiateChunkSize();
454            }
455        }
456    }
457
458    class StrategyChangeListener implements ItemListener, FocusListener, ActionListener {
459
460        protected void notifyStrategy() {
461            firePropertyChange(UPLOAD_STRATEGY_SPECIFICATION_PROP, null, getUploadStrategySpecification());
462        }
463
464        @Override
465        public void itemStateChanged(ItemEvent e) {
466            UploadStrategy strategy = getUploadStrategy();
467            if (strategy == null) return;
468            switch(strategy) {
469            case CHUNKED_DATASET_STRATEGY:
470                tfChunkSize.setEnabled(true);
471                tfChunkSize.requestFocusInWindow();
472                break;
473            default:
474                tfChunkSize.setEnabled(false);
475            }
476            notifyStrategy();
477        }
478
479        @Override
480        public void focusGained(FocusEvent arg0) {}
481
482        @Override
483        public void focusLost(FocusEvent arg0) {
484            notifyStrategy();
485        }
486
487        @Override
488        public void actionPerformed(ActionEvent arg0) {
489            notifyStrategy();
490        }
491    }
492}