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