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