001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions;
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.event.ActionEvent;
008    import java.awt.event.KeyEvent;
009    import java.util.LinkedList;
010    
011    import javax.swing.JOptionPane;
012    import javax.swing.SwingUtilities;
013    
014    import org.openstreetmap.josm.Main;
015    import org.openstreetmap.josm.actions.upload.ApiPreconditionCheckerHook;
016    import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook;
017    import org.openstreetmap.josm.actions.upload.UploadHook;
018    import org.openstreetmap.josm.actions.upload.ValidateUploadHook;
019    import org.openstreetmap.josm.data.APIDataSet;
020    import org.openstreetmap.josm.data.conflict.ConflictCollection;
021    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
022    import org.openstreetmap.josm.gui.help.HelpUtil;
023    import org.openstreetmap.josm.gui.io.UploadDialog;
024    import org.openstreetmap.josm.gui.io.UploadPrimitivesTask;
025    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
026    import org.openstreetmap.josm.gui.util.GuiHelper;
027    import org.openstreetmap.josm.tools.ImageProvider;
028    import org.openstreetmap.josm.tools.Shortcut;
029    
030    /**
031     * Action that opens a connection to the osm server and uploads all changes.
032     *
033     * An dialog is displayed asking the user to specify a rectangle to grab.
034     * The url and account settings from the preferences are used.
035     *
036     * If the upload fails this action offers various options to resolve conflicts.
037     *
038     * @author imi
039     */
040    public class UploadAction extends JosmAction{
041        /**
042         * The list of upload hooks. These hooks will be called one after the other
043         * when the user wants to upload data. Plugins can insert their own hooks here
044         * if they want to be able to veto an upload.
045         *
046         * Be default, the standard upload dialog is the only element in the list.
047         * Plugins should normally insert their code before that, so that the upload
048         * dialog is the last thing shown before upload really starts; on occasion
049         * however, a plugin might also want to insert something after that.
050         */
051        private static final LinkedList<UploadHook> uploadHooks = new LinkedList<UploadHook>();
052        static {
053            uploadHooks.add(new ValidateUploadHook());
054            /**
055             * Checks server capabilities before upload.
056             */
057            uploadHooks.add(new ApiPreconditionCheckerHook());
058    
059            /**
060             * Adjusts the upload order of new relations
061             */
062            uploadHooks.add(new RelationUploadOrderHook());
063        }
064    
065        /**
066         * Registers an upload hook. Adds the hook at the first position of the upload hooks.
067         *
068         * @param hook the upload hook. Ignored if null.
069         */
070        public static void registerUploadHook(UploadHook hook) {
071            if(hook == null) return;
072            if (!uploadHooks.contains(hook)) {
073                uploadHooks.add(0,hook);
074            }
075        }
076    
077        /**
078         * Unregisters an upload hook. Removes the hook from the list of upload hooks.
079         *
080         * @param hook the upload hook. Ignored if null.
081         */
082        public static void unregisterUploadHook(UploadHook hook) {
083            if(hook == null) return;
084            if (uploadHooks.contains(hook)) {
085                uploadHooks.remove(hook);
086            }
087        }
088    
089        public UploadAction() {
090            super(tr("Upload data"), "upload", tr("Upload all changes in the active data layer to the OSM server"),
091                    Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload data")), KeyEvent.VK_UP, Shortcut.CTRL_SHIFT), true);
092            putValue("help", ht("/Action/Upload"));
093        }
094    
095        /**
096         * Refreshes the enabled state
097         *
098         */
099        @Override
100        protected void updateEnabledState() {
101            setEnabled(getEditLayer() != null);
102        }
103    
104        public boolean checkPreUploadConditions(OsmDataLayer layer) {
105            return checkPreUploadConditions(layer, new APIDataSet(layer.data));
106        }
107    
108        protected static void alertUnresolvedConflicts(OsmDataLayer layer) {
109            HelpAwareOptionPane.showOptionDialog(
110                    Main.parent,
111                    tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>"
112                            + "You have to resolve them first.</html>", layer.getName()
113                    ),
114                    tr("Warning"),
115                    JOptionPane.WARNING_MESSAGE,
116                    HelpUtil.ht("/Action/Upload#PrimitivesParticipateInConflicts")
117            );
118        }
119        
120        /**
121         * returns true if the user wants to cancel, false if they
122         * want to continue
123         */
124        public static final boolean warnUploadDiscouraged(OsmDataLayer layer) {
125            return GuiHelper.warnUser(tr("Upload discouraged"),
126                    "<html>" +
127                    tr("You are about to upload data from the layer ''{0}''.<br /><br />"+
128                        "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+
129                        "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+
130                        "Are you sure you want to continue?", layer.getName())+
131                    "</html>",
132                    ImageProvider.get("upload"), tr("Ignore this hint and upload anyway"));
133        }
134    
135        /**
136         * Check whether the preconditions are met to upload data in <code>apiData</code>.
137         * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
138         * runs the installed {@link UploadHook}s.
139         *
140         * @param layer the source layer of the data to be uploaded
141         * @param apiData the data to be uploaded
142         * @return true, if the preconditions are met; false, otherwise
143         */
144        public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) {
145            if (layer.isUploadDiscouraged()) {
146                if (warnUploadDiscouraged(layer)) {
147                    return false;
148                }
149            }
150            ConflictCollection conflicts = layer.getConflicts();
151            if (apiData.participatesInConflict(conflicts)) {
152                alertUnresolvedConflicts(layer);
153                return false;
154            }
155            // Call all upload hooks in sequence.
156            // FIXME: this should become an asynchronous task
157            //
158            for (UploadHook hook : uploadHooks) {
159                if (!hook.checkUpload(apiData))
160                    return false;
161            }
162    
163            return true;
164        }
165    
166        /**
167         * Uploads data to the OSM API.
168         *
169         * @param layer the source layer for the data to upload
170         * @param apiData the primitives to be added, updated, or deleted
171         */
172        public void uploadData(final OsmDataLayer layer, APIDataSet apiData) {
173            if (apiData.isEmpty()) {
174                JOptionPane.showMessageDialog(
175                        Main.parent,
176                        tr("No changes to upload."),
177                        tr("Warning"),
178                        JOptionPane.INFORMATION_MESSAGE
179                );
180                return;
181            }
182            if (!checkPreUploadConditions(layer, apiData))
183                return;
184    
185            final UploadDialog dialog = UploadDialog.getUploadDialog();
186            // If we simply set the changeset comment here, it would be
187            // overridden by subsequent events in EDT that are caused by
188            // dialog creation. The current solution is to queue this operation
189            // after these events.
190            // TODO: find better way to initialize the comment field
191            SwingUtilities.invokeLater(new Runnable() {
192                public void run() {
193                    dialog.setDefaultChangesetTags(layer.data.getChangeSetTags());
194                }
195            });
196            dialog.setUploadedPrimitives(apiData);
197            dialog.setVisible(true);
198            if (dialog.isCanceled())
199                return;
200            dialog.rememberUserInput();
201    
202            Main.worker.execute(
203                    new UploadPrimitivesTask(
204                            UploadDialog.getUploadDialog().getUploadStrategySpecification(),
205                            layer,
206                            apiData,
207                            UploadDialog.getUploadDialog().getChangeset()
208                    )
209            );
210        }
211    
212        public void actionPerformed(ActionEvent e) {
213            if (!isEnabled())
214                return;
215            if (Main.map == null) {
216                JOptionPane.showMessageDialog(
217                        Main.parent,
218                        tr("Nothing to upload. Get some data first."),
219                        tr("Warning"),
220                        JOptionPane.WARNING_MESSAGE
221                );
222                return;
223            }
224            APIDataSet apiData = new APIDataSet(Main.main.getCurrentDataSet());
225            uploadData(Main.map.mapView.getEditLayer(), apiData);
226        }
227    }