001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.BorderLayout;
009import java.awt.Component;
010import java.awt.Dimension;
011import java.awt.FlowLayout;
012import java.awt.GridBagLayout;
013import java.awt.Image;
014import java.awt.event.ActionEvent;
015import java.awt.event.KeyEvent;
016import java.awt.event.WindowAdapter;
017import java.awt.event.WindowEvent;
018import java.beans.PropertyChangeEvent;
019import java.beans.PropertyChangeListener;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026
027import javax.swing.AbstractAction;
028import javax.swing.BorderFactory;
029import javax.swing.Icon;
030import javax.swing.ImageIcon;
031import javax.swing.JButton;
032import javax.swing.JComponent;
033import javax.swing.JDialog;
034import javax.swing.JOptionPane;
035import javax.swing.JPanel;
036import javax.swing.JTabbedPane;
037import javax.swing.KeyStroke;
038
039import org.openstreetmap.josm.Main;
040import org.openstreetmap.josm.data.APIDataSet;
041import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
042import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
043import org.openstreetmap.josm.data.Preferences.Setting;
044import org.openstreetmap.josm.data.osm.Changeset;
045import org.openstreetmap.josm.data.osm.OsmPrimitive;
046import org.openstreetmap.josm.gui.ExtendedDialog;
047import org.openstreetmap.josm.gui.HelpAwareOptionPane;
048import org.openstreetmap.josm.gui.SideButton;
049import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
050import org.openstreetmap.josm.gui.help.HelpUtil;
051import org.openstreetmap.josm.io.OsmApi;
052import org.openstreetmap.josm.tools.GBC;
053import org.openstreetmap.josm.tools.ImageProvider;
054import org.openstreetmap.josm.tools.InputMapUtils;
055import org.openstreetmap.josm.tools.Utils;
056import org.openstreetmap.josm.tools.WindowGeometry;
057
058/**
059 * This is a dialog for entering upload options like the parameters for
060 * the upload changeset and the strategy for opening/closing a changeset.
061 *
062 */
063public class UploadDialog extends JDialog implements PropertyChangeListener, PreferenceChangedListener{
064    /**  the unique instance of the upload dialog */
065    static private UploadDialog uploadDialog;
066
067    /**
068     * List of custom components that can be added by plugins at JOSM startup.
069     */
070    static private final Collection<Component> customComponents = new ArrayList<Component>();
071
072    /**
073     * Replies the unique instance of the upload dialog
074     *
075     * @return the unique instance of the upload dialog
076     */
077    static public UploadDialog getUploadDialog() {
078        if (uploadDialog == null) {
079            uploadDialog = new UploadDialog();
080        }
081        return uploadDialog;
082    }
083
084    /** the panel with the objects to upload */
085    private UploadedObjectsSummaryPanel pnlUploadedObjects;
086    /** the panel to select the changeset used */
087    private ChangesetManagementPanel pnlChangesetManagement;
088
089    private BasicUploadSettingsPanel pnlBasicUploadSettings;
090
091    private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel;
092
093    /** checkbox for selecting whether an atomic upload is to be used  */
094    private TagSettingsPanel pnlTagSettings;
095    /** the tabbed pane used below of the list of primitives  */
096    private JTabbedPane tpConfigPanels;
097    /** the upload button */
098    private JButton btnUpload;
099    private boolean canceled = false;
100
101    /** the changeset comment model keeping the state of the changeset comment */
102    private final ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel();
103    private final ChangesetCommentModel changesetSourceModel = new ChangesetCommentModel();
104
105    /**
106     * builds the content panel for the upload dialog
107     *
108     * @return the content panel
109     */
110    protected JPanel buildContentPanel() {
111        JPanel pnl = new JPanel(new GridBagLayout());
112        pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
113
114        // the panel with the list of uploaded objects
115        //
116        pnl.add(pnlUploadedObjects = new UploadedObjectsSummaryPanel(), GBC.eol().fill(GBC.BOTH));
117
118        // Custom components
119        for (Component c : customComponents) {
120            pnl.add(c, GBC.eol().fill(GBC.HORIZONTAL));
121        }
122
123        // a tabbed pane with configuration panels in the lower half
124        //
125        tpConfigPanels = new JTabbedPane() {
126            @Override
127            public Dimension getPreferredSize() {
128                // make sure the tabbed pane never grabs more space than necessary
129                //
130                return super.getMinimumSize();
131            }
132        };
133
134        tpConfigPanels.add(pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel, changesetSourceModel));
135        tpConfigPanels.setTitleAt(0, tr("Settings"));
136        tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use"));
137
138        tpConfigPanels.add(pnlTagSettings = new TagSettingsPanel(changesetCommentModel, changesetSourceModel));
139        tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
140        tpConfigPanels.setToolTipTextAt(1, tr("Apply tags to the changeset data is uploaded to"));
141
142        tpConfigPanels.add(pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel));
143        tpConfigPanels.setTitleAt(2, tr("Changesets"));
144        tpConfigPanels.setToolTipTextAt(2, tr("Manage open changesets and select a changeset to upload to"));
145
146        tpConfigPanels.add(pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel());
147        tpConfigPanels.setTitleAt(3, tr("Advanced"));
148        tpConfigPanels.setToolTipTextAt(3, tr("Configure advanced settings"));
149
150        pnl.add(tpConfigPanels, GBC.eol().fill(GBC.HORIZONTAL));
151        return pnl;
152    }
153
154    /**
155     * builds the panel with the OK and CANCEL buttons
156     *
157     * @return The panel with the OK and CANCEL buttons
158     */
159    protected JPanel buildActionPanel() {
160        JPanel pnl = new JPanel();
161        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
162        pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
163
164        // -- upload button
165        UploadAction uploadAction = new UploadAction();
166        pnl.add(btnUpload = new SideButton(uploadAction));
167        btnUpload.setFocusable(true);
168        InputMapUtils.enableEnter(btnUpload);
169
170        // -- cancel button
171        CancelAction cancelAction = new CancelAction();
172        pnl.add(new SideButton(cancelAction));
173        getRootPane().registerKeyboardAction(
174                cancelAction,
175                KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0),
176                JComponent.WHEN_IN_FOCUSED_WINDOW
177        );
178        pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload"))));
179        HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/Upload"));
180        return pnl;
181    }
182
183    /**
184     * builds the gui
185     */
186    protected void build() {
187        setTitle(tr("Upload to ''{0}''", OsmApi.getOsmApi().getBaseUrl()));
188        getContentPane().setLayout(new BorderLayout());
189        getContentPane().add(buildContentPanel(), BorderLayout.CENTER);
190        getContentPane().add(buildActionPanel(), BorderLayout.SOUTH);
191
192        addWindowListener(new WindowEventHandler());
193
194
195        // make sure the configuration panels listen to each other
196        // changes
197        //
198        pnlChangesetManagement.addPropertyChangeListener(
199                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
200        );
201        pnlChangesetManagement.addPropertyChangeListener(this);
202        pnlUploadedObjects.addPropertyChangeListener(
203                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
204        );
205        pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel);
206        pnlUploadStrategySelectionPanel.addPropertyChangeListener(
207                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
208        );
209
210
211        // users can click on either of two links in the upload parameter
212        // summary handler. This installs the handler for these two events.
213        // We simply select the appropriate tab in the tabbed pane with the
214        // configuration dialogs.
215        //
216        pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener(
217                new ConfigurationParameterRequestHandler() {
218                    @Override
219                    public void handleUploadStrategyConfigurationRequest() {
220                        tpConfigPanels.setSelectedIndex(3);
221                    }
222                    @Override
223                    public void handleChangesetConfigurationRequest() {
224                        tpConfigPanels.setSelectedIndex(2);
225                    }
226                }
227        );
228
229        pnlBasicUploadSettings.setUploadTagDownFocusTraversalHandlers(
230                new AbstractAction() {
231                    @Override
232                    public void actionPerformed(ActionEvent e) {
233                        btnUpload.requestFocusInWindow();
234                    }
235                }
236        );
237
238        Main.pref.addPreferenceChangeListener(this);
239    }
240
241    /**
242     * constructor
243     */
244    public UploadDialog() {
245        super(JOptionPane.getFrameForComponent(Main.parent), ModalityType.DOCUMENT_MODAL);
246        build();
247    }
248
249    /**
250     * Sets the collection of primitives to upload
251     *
252     * @param toUpload the dataset with the objects to upload. If null, assumes the empty
253     * set of objects to upload
254     *
255     */
256    public void setUploadedPrimitives(APIDataSet toUpload) {
257        if (toUpload == null) {
258            List<OsmPrimitive> emptyList = Collections.emptyList();
259            pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList);
260            return;
261        }
262        pnlUploadedObjects.setUploadedPrimitives(
263                toUpload.getPrimitivesToAdd(),
264                toUpload.getPrimitivesToUpdate(),
265                toUpload.getPrimitivesToDelete()
266        );
267    }
268
269    /**
270     * Remembers the user input in the preference settings
271     */
272    public void rememberUserInput() {
273        pnlBasicUploadSettings.rememberUserInput();
274        pnlUploadStrategySelectionPanel.rememberUserInput();
275    }
276
277    /**
278     * Initializes the panel for user input
279     */
280    public void startUserInput() {
281        tpConfigPanels.setSelectedIndex(0);
282        pnlBasicUploadSettings.startUserInput();
283        pnlTagSettings.startUserInput();
284        pnlTagSettings.initFromChangeset(pnlChangesetManagement.getSelectedChangeset());
285        pnlUploadStrategySelectionPanel.initFromPreferences();
286        UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
287        pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification());
288        pnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
289        pnl.setNumObjects(pnlUploadedObjects.getNumObjectsToUpload());
290    }
291
292    /**
293     * Replies the current changeset
294     *
295     * @return the current changeset
296     */
297    public Changeset getChangeset() {
298        Changeset cs = pnlChangesetManagement.getSelectedChangeset();
299        if (cs == null) {
300            cs = new Changeset();
301        }
302        cs.setKeys(pnlTagSettings.getTags(false));
303        return cs;
304    }
305
306    public void setSelectedChangesetForNextUpload(Changeset cs) {
307        pnlChangesetManagement.setSelectedChangesetForNextUpload(cs);
308    }
309
310    public Map<String, String> getDefaultChangesetTags() {
311        return pnlTagSettings.getDefaultTags();
312    }
313
314    public void setDefaultChangesetTags(Map<String, String> tags) {
315        pnlTagSettings.setDefaultTags(tags);
316         for (Entry<String, String> entry: tags.entrySet()) {
317            if ("comment".equals(entry.getKey())) {
318                changesetCommentModel.setComment(entry.getValue());
319            }
320        }
321    }
322
323    /**
324     * Replies the {@link UploadStrategySpecification} the user entered in the dialog.
325     *
326     * @return the {@link UploadStrategySpecification} the user entered in the dialog.
327     */
328    public UploadStrategySpecification getUploadStrategySpecification() {
329        UploadStrategySpecification spec = pnlUploadStrategySelectionPanel.getUploadStrategySpecification();
330        spec.setCloseChangesetAfterUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
331        return spec;
332    }
333
334    /**
335     * Returns the current value for the upload comment
336     *
337     * @return the current value for the upload comment
338     */
339    protected String getUploadComment() {
340        return changesetCommentModel.getComment();
341    }
342
343    /**
344     * Returns the current value for the changeset source
345     *
346     * @return the current value for the changeset source
347     */
348    protected String getUploadSource() {
349        return changesetSourceModel.getComment();
350    }
351
352    /**
353     * Returns true if the dialog was canceled
354     *
355     * @return true if the dialog was canceled
356     */
357    public boolean isCanceled() {
358        return canceled;
359    }
360
361    /**
362     * Sets whether the dialog was canceled
363     *
364     * @param canceled true if the dialog is canceled
365     */
366    protected void setCanceled(boolean canceled) {
367        this.canceled = canceled;
368    }
369
370    @Override
371    public void setVisible(boolean visible) {
372        if (visible) {
373            new WindowGeometry(
374                    getClass().getName() + ".geometry",
375                    WindowGeometry.centerInWindow(
376                            Main.parent,
377                            new Dimension(400,600)
378                    )
379            ).applySafe(this);
380            startUserInput();
381        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
382            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
383        }
384        super.setVisible(visible);
385    }
386
387    /**
388     * Adds a custom component to this dialog.
389     * Custom components added at JOSM startup are displayed between the objects list and the properties tab pane.
390     * @param c The custom component to add. If {@code null}, this method does nothing.
391     * @return {@code true} if the collection of custom components changed as a result of the call
392     * @since 5842
393     */
394    public static boolean addCustomComponent(Component c) {
395        if (c != null) {
396            return customComponents.add(c);
397        }
398        return false;
399    }
400
401    /**
402     * Handles an upload
403     *
404     */
405    class UploadAction extends AbstractAction {
406        public UploadAction() {
407            putValue(NAME, tr("Upload Changes"));
408            putValue(SMALL_ICON, ImageProvider.get("upload"));
409            putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives"));
410        }
411
412        /**
413         * Displays a warning message indicating that the upload comment is empty/short.
414         * @return true if the user wants to revisit, false if they want to continue
415         */
416        protected boolean warnUploadComment() {
417            return warnUploadTag(
418                    tr("Please revise upload comment"),
419                    tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" +
420                            "This is technically allowed, but please consider that many users who are<br />" +
421                            "watching changes in their area depend on meaningful changeset comments<br />" +
422                            "to understand what is going on!<br /><br />" +
423                            "If you spend a minute now to explain your change, you will make life<br />" +
424                            "easier for many other mappers."),
425                    "upload_comment_is_empty_or_very_short"
426            );
427        }
428
429        /**
430         * Displays a warning message indicating that no changeset source is given.
431         * @return true if the user wants to revisit, false if they want to continue
432         */
433        protected boolean warnUploadSource() {
434            return warnUploadTag(
435                    tr("Please specify a changeset source"),
436                    tr("You did not specify a source for your changes.<br />" +
437                            "This is technically allowed, but it assists other users <br />" +
438                            "to understand the origins of the data.<br /><br />" +
439                            "If you spend a minute now to explain your change, you will make life<br />" +
440                            "easier for many other mappers."),
441                    "upload_source_is_empty"
442            );
443        }
444
445        protected boolean warnUploadTag(final String title, final String message, final String togglePref) {
446            ExtendedDialog dlg = new ExtendedDialog(UploadDialog.this,
447                    title,
448                    new String[] {tr("Revise"), tr("Cancel"), tr("Continue as is")});
449            dlg.setContent("<html>" + message + "</html>");
450            dlg.setButtonIcons(new Icon[] {
451                    ImageProvider.get("ok"),
452                    ImageProvider.get("cancel"),
453                    ImageProvider.overlay(
454                            ImageProvider.get("upload"),
455                            new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)),
456                            ImageProvider.OverlayPosition.SOUTHEAST)});
457            dlg.setToolTipTexts(new String[] {
458                    tr("Return to the previous dialog to enter a more descriptive comment"),
459                    tr("Cancel and return to the previous dialog"),
460                    tr("Ignore this hint and upload anyway")});
461            dlg.setIcon(JOptionPane.WARNING_MESSAGE);
462            dlg.toggleEnable(togglePref);
463            dlg.setToggleCheckboxText(tr("Do not show this message again"));
464            dlg.setCancelButton(1, 2);
465            return dlg.showDialog().getValue() != 3;
466        }
467
468        protected void warnIllegalChunkSize() {
469            HelpAwareOptionPane.showOptionDialog(
470                    UploadDialog.this,
471                    tr("Please enter a valid chunk size first"),
472                    tr("Illegal chunk size"),
473                    JOptionPane.ERROR_MESSAGE,
474                    ht("/Dialog/Upload#IllegalChunkSize")
475            );
476        }
477
478        @Override
479        public void actionPerformed(ActionEvent e) {
480            if ((getUploadComment().trim().length() < 10 && warnUploadComment()) /* abort for missing comment */
481                    || (getUploadSource().trim().isEmpty() && warnUploadSource()) /* abort for missing changeset source */
482                    ) {
483                tpConfigPanels.setSelectedIndex(0);
484                pnlBasicUploadSettings.initEditingOfUploadComment();
485                return;
486            }
487
488            /* test for empty tags in the changeset metadata and proceed only after user's confirmation.
489             * though, accept if key and value are empty (cf. xor). */
490            List<String> emptyChangesetTags = new ArrayList<String>();
491            for (final Entry<String, String> i : pnlTagSettings.getTags(true).entrySet()) {
492                final boolean isKeyEmpty = i.getKey() == null || i.getKey().trim().isEmpty();
493                final boolean isValueEmpty = i.getValue() == null || i.getValue().trim().isEmpty();
494                final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey());
495                if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) {
496                    emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue()));
497                }
498            }
499            if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(
500                    Main.parent,
501                    trn(
502                            "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>",
503                            "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>",
504                            emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)),
505                    tr("Empty metadata"),
506                    JOptionPane.OK_CANCEL_OPTION,
507                    JOptionPane.WARNING_MESSAGE
508            )) {
509                tpConfigPanels.setSelectedIndex(0);
510                pnlBasicUploadSettings.initEditingOfUploadComment();
511                return;
512            }
513
514            UploadStrategySpecification strategy = getUploadStrategySpecification();
515            if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)) {
516                if (strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
517                    warnIllegalChunkSize();
518                    tpConfigPanels.setSelectedIndex(0);
519                    return;
520                }
521            }
522            setCanceled(false);
523            setVisible(false);
524        }
525    }
526
527    /**
528     * Action for canceling the dialog
529     *
530     */
531    class CancelAction extends AbstractAction {
532        public CancelAction() {
533            putValue(NAME, tr("Cancel"));
534            putValue(SMALL_ICON, ImageProvider.get("cancel"));
535            putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing"));
536        }
537
538        @Override
539        public void actionPerformed(ActionEvent e) {
540            setCanceled(true);
541            setVisible(false);
542        }
543    }
544
545    /**
546     * Listens to window closing events and processes them as cancel events.
547     * Listens to window open events and initializes user input
548     *
549     */
550    class WindowEventHandler extends WindowAdapter {
551        @Override
552        public void windowClosing(WindowEvent e) {
553            setCanceled(true);
554        }
555
556        @Override
557        public void windowActivated(WindowEvent arg0) {
558            if (tpConfigPanels.getSelectedIndex() == 0) {
559                pnlBasicUploadSettings.initEditingOfUploadComment();
560            }
561        }
562    }
563
564    /* -------------------------------------------------------------------------- */
565    /* Interface PropertyChangeListener                                           */
566    /* -------------------------------------------------------------------------- */
567    @Override
568    public void propertyChange(PropertyChangeEvent evt) {
569        if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) {
570            Changeset cs = (Changeset)evt.getNewValue();
571            if (cs == null) {
572                tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
573            } else {
574                tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId()));
575            }
576        }
577    }
578
579    /* -------------------------------------------------------------------------- */
580    /* Interface PreferenceChangedListener                                        */
581    /* -------------------------------------------------------------------------- */
582    @Override
583    public void preferenceChanged(PreferenceChangeEvent e) {
584        if (e.getKey() == null || ! e.getKey().equals("osm-server.url"))
585            return;
586        final Setting<?> newValue = e.getNewValue();
587        final String url;
588        if (newValue == null || newValue.getValue() == null) {
589            url = OsmApi.getOsmApi().getBaseUrl();
590        } else {
591            url = newValue.getValue().toString();
592        }
593        setTitle(tr("Upload to ''{0}''", url));
594    }
595}