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