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