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 }