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