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