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