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