001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.io;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    
007    import java.awt.BorderLayout;
008    import java.awt.Dimension;
009    import java.awt.FlowLayout;
010    import java.awt.Image;
011    import java.awt.event.ActionEvent;
012    import java.awt.event.KeyEvent;
013    import java.awt.event.WindowAdapter;
014    import java.awt.event.WindowEvent;
015    import java.beans.PropertyChangeEvent;
016    import java.beans.PropertyChangeListener;
017    import java.util.Collections;
018    import java.util.List;
019    import java.util.Map;
020    
021    import javax.swing.AbstractAction;
022    import javax.swing.BorderFactory;
023    import javax.swing.Icon;
024    import javax.swing.ImageIcon;
025    import javax.swing.JButton;
026    import javax.swing.JComponent;
027    import javax.swing.JDialog;
028    import javax.swing.JOptionPane;
029    import javax.swing.JPanel;
030    import javax.swing.JTabbedPane;
031    import javax.swing.KeyStroke;
032    
033    import org.openstreetmap.josm.Main;
034    import org.openstreetmap.josm.data.APIDataSet;
035    import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
036    import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
037    import org.openstreetmap.josm.data.Preferences.Setting;
038    import org.openstreetmap.josm.data.osm.Changeset;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.gui.ExtendedDialog;
041    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
042    import org.openstreetmap.josm.gui.SideButton;
043    import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
044    import org.openstreetmap.josm.gui.help.HelpUtil;
045    import org.openstreetmap.josm.io.OsmApi;
046    import org.openstreetmap.josm.tools.ImageProvider;
047    import org.openstreetmap.josm.tools.InputMapUtils;
048    import org.openstreetmap.josm.tools.WindowGeometry;
049    
050    /**
051     * This is a dialog for entering upload options like the parameters for
052     * the upload changeset and the strategy for opening/closing a changeset.
053     *
054     */
055    public class UploadDialog extends JDialog implements PropertyChangeListener, PreferenceChangedListener{
056        /**  the unique instance of the upload dialog */
057        static private UploadDialog uploadDialog;
058    
059        /**
060         * Replies the unique instance of the upload dialog
061         *
062         * @return the unique instance of the upload dialog
063         */
064        static public UploadDialog getUploadDialog() {
065            if (uploadDialog == null) {
066                uploadDialog = new UploadDialog();
067            }
068            return uploadDialog;
069        }
070    
071        /** the panel with the objects to upload */
072        private UploadedObjectsSummaryPanel pnlUploadedObjects;
073        /** the panel to select the changeset used */
074        private ChangesetManagementPanel pnlChangesetManagement;
075    
076        private BasicUploadSettingsPanel pnlBasicUploadSettings;
077    
078        private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel;
079    
080        /** checkbox for selecting whether an atomic upload is to be used  */
081        private TagSettingsPanel pnlTagSettings;
082        /** the tabbed pane used below of the list of primitives  */
083        private JTabbedPane tpConfigPanels;
084        /** the upload button */
085        private JButton btnUpload;
086        private boolean canceled = false;
087    
088        /** the changeset comment model keeping the state of the changeset comment */
089        private ChangesetCommentModel changesetCommentModel;
090    
091        /**
092         * builds the content panel for the upload dialog
093         *
094         * @return the content panel
095         */
096        protected JPanel buildContentPanel() {
097            JPanel pnl = new JPanel();
098            pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
099            pnl.setLayout(new BorderLayout());
100    
101            // the panel with the list of uploaded objects
102            //
103            pnl.add(pnlUploadedObjects = new UploadedObjectsSummaryPanel(), BorderLayout.CENTER);
104    
105            // a tabbed pane with two configuration panels in the
106            // lower half
107            //
108            tpConfigPanels = new JTabbedPane() {
109                @Override
110                public Dimension getPreferredSize() {
111                    // make sure the tabbed pane never grabs more space than necessary
112                    //
113                    return super.getMinimumSize();
114                }
115            };
116            tpConfigPanels.add(new JPanel());
117            tpConfigPanels.add(new JPanel());
118            tpConfigPanels.add(new JPanel());
119            tpConfigPanels.add(new JPanel());
120    
121            changesetCommentModel = new ChangesetCommentModel();
122    
123            tpConfigPanels.setComponentAt(0, pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel));
124            tpConfigPanels.setTitleAt(0, tr("Settings"));
125            tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use"));
126    
127            tpConfigPanels.setComponentAt(1,pnlTagSettings = new TagSettingsPanel(changesetCommentModel));
128            tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
129            tpConfigPanels.setToolTipTextAt(1, tr("Apply tags to the changeset data is uploaded to"));
130    
131            tpConfigPanels.setComponentAt(2,pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel));
132            tpConfigPanels.setTitleAt(2, tr("Changesets"));
133            tpConfigPanels.setToolTipTextAt(2, tr("Manage open changesets and select a changeset to upload to"));
134    
135            tpConfigPanels.setComponentAt(3, pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel());
136            tpConfigPanels.setTitleAt(3, tr("Advanced"));
137            tpConfigPanels.setToolTipTextAt(3, tr("Configure advanced settings"));
138    
139            pnl.add(tpConfigPanels, BorderLayout.SOUTH);
140            return pnl;
141        }
142    
143        /**
144         * builds the panel with the OK and CANCEL buttons
145         *
146         * @return
147         */
148        protected JPanel buildActionPanel() {
149            JPanel pnl = new JPanel();
150            pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
151            pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
152    
153            // -- upload button
154            UploadAction uploadAction = new UploadAction();
155            pnl.add(btnUpload = new SideButton(uploadAction));
156            btnUpload.setFocusable(true);
157            InputMapUtils.enableEnter(btnUpload);
158    
159            // -- cancel button
160            CancelAction cancelAction = new CancelAction();
161            pnl.add(new SideButton(cancelAction));
162            getRootPane().registerKeyboardAction(
163                    cancelAction,
164                    KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0),
165                    JComponent.WHEN_IN_FOCUSED_WINDOW
166            );
167            pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload"))));
168            HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/Upload"));
169            return pnl;
170        }
171    
172        /**
173         * builds the gui
174         */
175        protected void build() {
176            setTitle(tr("Upload to ''{0}''", OsmApi.getOsmApi().getBaseUrl()));
177            getContentPane().setLayout(new BorderLayout());
178            getContentPane().add(buildContentPanel(), BorderLayout.CENTER);
179            getContentPane().add(buildActionPanel(), BorderLayout.SOUTH);
180    
181            addWindowListener(new WindowEventHandler());
182    
183    
184            // make sure the configuration panels listen to each other
185            // changes
186            //
187            pnlChangesetManagement.addPropertyChangeListener(
188                    pnlBasicUploadSettings.getUploadParameterSummaryPanel()
189            );
190            pnlChangesetManagement.addPropertyChangeListener(this);
191            pnlUploadedObjects.addPropertyChangeListener(
192                    pnlBasicUploadSettings.getUploadParameterSummaryPanel()
193            );
194            pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel);
195            pnlUploadStrategySelectionPanel.addPropertyChangeListener(
196                    pnlBasicUploadSettings.getUploadParameterSummaryPanel()
197            );
198    
199    
200            // users can click on either of two links in the upload parameter
201            // summary handler. This installs the handler for these two events.
202            // We simply select the appropriate tab in the tabbed pane with the
203            // configuration dialogs.
204            //
205            pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener(
206                    new ConfigurationParameterRequestHandler() {
207                        public void handleUploadStrategyConfigurationRequest() {
208                            tpConfigPanels.setSelectedIndex(3);
209                        }
210                        public void handleChangesetConfigurationRequest() {
211                            tpConfigPanels.setSelectedIndex(2);
212                        }
213                    }
214            );
215    
216            pnlBasicUploadSettings.setUploadCommentDownFocusTraversalHandler(
217                    new AbstractAction() {
218                        public void actionPerformed(ActionEvent e) {
219                            btnUpload.requestFocusInWindow();
220                        }
221                    }
222            );
223    
224            Main.pref.addPreferenceChangeListener(this);
225        }
226    
227        /**
228         * constructor
229         */
230        public UploadDialog() {
231            super(JOptionPane.getFrameForComponent(Main.parent), ModalityType.DOCUMENT_MODAL);
232            build();
233        }
234    
235        /**
236         * Sets the collection of primitives to upload
237         *
238         * @param toUpload the dataset with the objects to upload. If null, assumes the empty
239         * set of objects to upload
240         *
241         */
242        public void setUploadedPrimitives(APIDataSet toUpload) {
243            if (toUpload == null) {
244                List<OsmPrimitive> emptyList = Collections.emptyList();
245                pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList);
246                return;
247            }
248            pnlUploadedObjects.setUploadedPrimitives(
249                    toUpload.getPrimitivesToAdd(),
250                    toUpload.getPrimitivesToUpdate(),
251                    toUpload.getPrimitivesToDelete()
252            );
253        }
254    
255        /**
256         * Remembers the user input in the preference settings
257         */
258        public void rememberUserInput() {
259            pnlBasicUploadSettings.rememberUserInput();
260            pnlUploadStrategySelectionPanel.rememberUserInput();
261        }
262    
263        /**
264         * Initializes the panel for user input
265         */
266        public void startUserInput() {
267            tpConfigPanels.setSelectedIndex(0);
268            pnlBasicUploadSettings.startUserInput();
269            pnlTagSettings.startUserInput();
270            pnlTagSettings.initFromChangeset(pnlChangesetManagement.getSelectedChangeset());
271            pnlUploadStrategySelectionPanel.initFromPreferences();
272            UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
273            pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification());
274            pnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
275            pnl.setNumObjects(pnlUploadedObjects.getNumObjectsToUpload());
276        }
277    
278        /**
279         * Replies the current changeset
280         *
281         * @return the current changeset
282         */
283        public Changeset getChangeset() {
284            Changeset cs = pnlChangesetManagement.getSelectedChangeset();
285            if (cs == null) {
286                cs = new Changeset();
287            }
288            cs.setKeys(pnlTagSettings.getTags());
289            return cs;
290        }
291    
292        public void setSelectedChangesetForNextUpload(Changeset cs) {
293            pnlChangesetManagement.setSelectedChangesetForNextUpload(cs);
294        }
295    
296        public Map<String, String> getDefaultChangesetTags() {
297            return pnlTagSettings.getDefaultTags();
298        }
299    
300        public void setDefaultChangesetTags(Map<String, String> tags) {
301            pnlTagSettings.setDefaultTags(tags);
302             for (String key: tags.keySet()) { 
303                if (key.equals("comment")) { 
304                    changesetCommentModel.setComment(tags.get(key));
305                } 
306            } 
307        }
308    
309        /**
310         * Replies the {@link UploadStrategySpecification} the user entered in the dialog.
311         *
312         * @return the {@link UploadStrategySpecification} the user entered in the dialog.
313         */
314        public UploadStrategySpecification getUploadStrategySpecification() {
315            UploadStrategySpecification spec = pnlUploadStrategySelectionPanel.getUploadStrategySpecification();
316            spec.setCloseChangesetAfterUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
317            return spec;
318        }
319    
320        /**
321         * Returns the current value for the upload comment
322         *
323         * @return the current value for the upload comment
324         */
325        protected String getUploadComment() {
326            return changesetCommentModel.getComment();
327        }
328    
329        /**
330         * Returns true if the dialog was canceled
331         *
332         * @return true if the dialog was canceled
333         */
334        public boolean isCanceled() {
335            return canceled;
336        }
337    
338        /**
339         * Sets whether the dialog was canceled
340         *
341         * @param canceled true if the dialog is canceled
342         */
343        protected void setCanceled(boolean canceled) {
344            this.canceled = canceled;
345        }
346    
347        @Override
348        public void setVisible(boolean visible) {
349            if (visible) {
350                new WindowGeometry(
351                        getClass().getName() + ".geometry",
352                        WindowGeometry.centerInWindow(
353                                Main.parent,
354                                new Dimension(400,600)
355                        )
356                ).applySafe(this);
357                startUserInput();
358            } else if (!visible && isShowing()){
359                new WindowGeometry(this).remember(getClass().getName() + ".geometry");
360            }
361            super.setVisible(visible);
362        }
363    
364        /**
365         * Handles an upload
366         *
367         */
368        class UploadAction extends AbstractAction {
369            public UploadAction() {
370                putValue(NAME, tr("Upload Changes"));
371                putValue(SMALL_ICON, ImageProvider.get("upload"));
372                putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives"));
373            }
374    
375            /**
376             * returns true if the user wants to revisit, false if they
377             * want to continue
378             */
379            protected boolean warnUploadComment() {
380                ExtendedDialog dlg = new ExtendedDialog(UploadDialog.this,
381                        tr("Please revise upload comment"),
382                        new String[] {tr("Revise"), tr("Cancel"), tr("Continue as is")});
383                dlg.setContent("<html>" +
384                        tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" +
385                                "This is technically allowed, but please consider that many users who are<br />" +
386                                "watching changes in their area depend on meaningful changeset comments<br />" +
387                                "to understand what is going on!<br /><br />" +
388                                "If you spend a minute now to explain your change, you will make life<br />" +
389                        "easier for many other mappers.") +
390                "</html>");
391                dlg.setButtonIcons(new Icon[] {
392                        ImageProvider.get("ok"),
393                        ImageProvider.get("cancel"),
394                        ImageProvider.overlay(
395                                ImageProvider.get("upload"),
396                                new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)),
397                                ImageProvider.OverlayPosition.SOUTHEAST)});
398                dlg.setToolTipTexts(new String[] {
399                        tr("Return to the previous dialog to enter a more descriptive comment"),
400                        tr("Cancel and return to the previous dialog"),
401                        tr("Ignore this hint and upload anyway")});
402                dlg.setIcon(JOptionPane.WARNING_MESSAGE);
403                dlg.toggleEnable("upload_comment_is_empty_or_very_short");
404                dlg.setToggleCheckboxText(tr("Do not show this message again"));
405                dlg.setCancelButton(1, 2);
406                return dlg.showDialog().getValue() != 3;
407            }
408    
409            protected void warnIllegalChunkSize() {
410                HelpAwareOptionPane.showOptionDialog(
411                        UploadDialog.this,
412                        tr("Please enter a valid chunk size first"),
413                        tr("Illegal chunk size"),
414                        JOptionPane.ERROR_MESSAGE,
415                        ht("/Dialog/Upload#IllegalChunkSize")
416                );
417            }
418    
419            public void actionPerformed(ActionEvent e) {
420                if (getUploadComment().trim().length() < 10) {
421                    if (warnUploadComment())
422                    {
423                        tpConfigPanels.setSelectedIndex(0);
424                        pnlBasicUploadSettings.initEditingOfUploadComment();
425                        return;
426                    }
427                }
428                UploadStrategySpecification strategy = getUploadStrategySpecification();
429                if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)) {
430                    if (strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
431                        warnIllegalChunkSize();
432                        tpConfigPanels.setSelectedIndex(0);
433                        return;
434                    }
435                }
436                setCanceled(false);
437                setVisible(false);
438            }
439        }
440    
441        /**
442         * Action for canceling the dialog
443         *
444         */
445        class CancelAction extends AbstractAction {
446            public CancelAction() {
447                putValue(NAME, tr("Cancel"));
448                putValue(SMALL_ICON, ImageProvider.get("cancel"));
449                putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing"));
450            }
451    
452            public void actionPerformed(ActionEvent e) {
453                setCanceled(true);
454                setVisible(false);
455            }
456        }
457    
458        /**
459         * Listens to window closing events and processes them as cancel events.
460         * Listens to window open events and initializes user input
461         *
462         */
463        class WindowEventHandler extends WindowAdapter {
464            @Override
465            public void windowClosing(WindowEvent e) {
466                setCanceled(true);
467            }
468    
469            @Override
470            public void windowOpened(WindowEvent e) {
471                //startUserInput();
472            }
473    
474            @Override
475            public void windowActivated(WindowEvent arg0) {
476                if (tpConfigPanels.getSelectedIndex() == 0) {
477                    pnlBasicUploadSettings.initEditingOfUploadComment();
478                }
479            }
480        }
481    
482        /* -------------------------------------------------------------------------- */
483        /* Interface PropertyChangeListener                                           */
484        /* -------------------------------------------------------------------------- */
485        public void propertyChange(PropertyChangeEvent evt) {
486            if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) {
487                Changeset cs = (Changeset)evt.getNewValue();
488                if (cs == null) {
489                    tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
490                } else {
491                    tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId()));
492                }
493            }
494        }
495    
496        /* -------------------------------------------------------------------------- */
497        /* Interface PreferenceChangedListener                                        */
498        /* -------------------------------------------------------------------------- */
499        public void preferenceChanged(PreferenceChangeEvent e) {
500            if (e.getKey() == null || ! e.getKey().equals("osm-server.url"))
501                return;
502            final Setting<?> newValue = e.getNewValue();
503            final String url;
504            if (newValue == null || newValue.getValue() == null) {
505                url = OsmApi.getOsmApi().getBaseUrl();
506            } else {
507                url = newValue.getValue().toString();
508            }
509            setTitle(tr("Upload to ''{0}''", url));
510        }
511    }