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