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