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